From 9e839ee8260d389f46dadf53d643e955d9bf2c00 Mon Sep 17 00:00:00 2001 From: DigiJ Date: Fri, 13 Mar 2026 12:39:02 -0700 Subject: [PATCH] =?UTF-8?q?Initial=20commit=20=E2=80=94=20SETEC=20LABS=20M?= =?UTF-8?q?anager=20(Setec=5FCDM)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitignore | 55 + manager.bat | 11 + reload.bat | 7 + resources/setec_labs_logo.svg | 67 + setec-web/agent/go.mod | 3 + setec-web/agent/main.go | 189 ++ setec-web/aide.py | 141 ++ setec-web/app.py | 2724 ++++++++++++++++++++++++++ setec-web/audit.py | 69 + setec-web/backup.py | 179 ++ setec-web/chkrootkit.py | 106 + setec-web/clamav.py | 187 ++ setec-web/config.py | 61 + setec-web/cowrie.py | 175 ++ setec-web/csf.py | 134 ++ setec-web/ddos.py | 464 +++++ setec-web/detector.py | 437 +++++ setec-web/dns_client.py | 110 ++ setec-web/docker_store.py | 709 +++++++ setec-web/e2e.py | 76 + setec-web/firewalld.py | 96 + setec-web/hardening.py | 220 +++ setec-web/hosting.py | 105 + setec-web/iptables.py | 209 ++ setec-web/lynis.py | 98 + setec-web/modsecurity.py | 188 ++ setec-web/monitoring.py | 359 ++++ setec-web/nftables.py | 110 ++ setec-web/ossec.py | 113 ++ setec-web/requirements.txt | 4 + setec-web/rkhunter.py | 152 ++ setec-web/run.bat | 13 + setec-web/sanitize.py | 101 + setec-web/sec_updates.py | 141 ++ setec-web/security_apps.py | 242 +++ setec-web/ssh_client.py | 126 ++ setec-web/ssl_audit.py | 105 + setec-web/static/setec_labs_logo.svg | 67 + setec-web/templates/base.html | 299 +++ setec-web/templates/configs.html | 222 +++ setec-web/templates/dashboard.html | 54 + setec-web/templates/detect.html | 140 ++ setec-web/templates/dns.html | 101 + setec-web/templates/docker.html | 334 ++++ setec-web/templates/docs.html | 609 ++++++ setec-web/templates/fail2ban.html | 168 ++ setec-web/templates/files.html | 93 + setec-web/templates/firewall.html | 990 ++++++++++ setec-web/templates/frontpage.html | 182 ++ setec-web/templates/nginx.html | 107 + setec-web/templates/security.html | 1598 +++++++++++++++ setec-web/templates/settings.html | 216 ++ setec-web/templates/smtp.html | 161 ++ setec-web/templates/terminal.html | 83 + setec-web/templates/wizard.html | 516 +++++ setec-web/wsgi.py | 53 + updates/sec/debian12_0326.sec | 55 + updates/sec/debian13_0326.sec | 53 + updates/sec/ubuntu2204_0326.sec | 55 + updates/sec/ubuntu2210_0326.sec | 49 + updates/sec/ubuntu2404_0326.sec | 57 + updates/sec/ubuntu2410_0326.sec | 57 + 62 files changed, 14605 insertions(+) create mode 100644 .gitignore create mode 100644 manager.bat create mode 100644 reload.bat create mode 100644 resources/setec_labs_logo.svg create mode 100644 setec-web/agent/go.mod create mode 100644 setec-web/agent/main.go create mode 100644 setec-web/aide.py create mode 100644 setec-web/app.py create mode 100644 setec-web/audit.py create mode 100644 setec-web/backup.py create mode 100644 setec-web/chkrootkit.py create mode 100644 setec-web/clamav.py create mode 100644 setec-web/config.py create mode 100644 setec-web/cowrie.py create mode 100644 setec-web/csf.py create mode 100644 setec-web/ddos.py create mode 100644 setec-web/detector.py create mode 100644 setec-web/dns_client.py create mode 100644 setec-web/docker_store.py create mode 100644 setec-web/e2e.py create mode 100644 setec-web/firewalld.py create mode 100644 setec-web/hardening.py create mode 100644 setec-web/hosting.py create mode 100644 setec-web/iptables.py create mode 100644 setec-web/lynis.py create mode 100644 setec-web/modsecurity.py create mode 100644 setec-web/monitoring.py create mode 100644 setec-web/nftables.py create mode 100644 setec-web/ossec.py create mode 100644 setec-web/requirements.txt create mode 100644 setec-web/rkhunter.py create mode 100644 setec-web/run.bat create mode 100644 setec-web/sanitize.py create mode 100644 setec-web/sec_updates.py create mode 100644 setec-web/security_apps.py create mode 100644 setec-web/ssh_client.py create mode 100644 setec-web/ssl_audit.py create mode 100644 setec-web/static/setec_labs_logo.svg create mode 100644 setec-web/templates/base.html create mode 100644 setec-web/templates/configs.html create mode 100644 setec-web/templates/dashboard.html create mode 100644 setec-web/templates/detect.html create mode 100644 setec-web/templates/dns.html create mode 100644 setec-web/templates/docker.html create mode 100644 setec-web/templates/docs.html create mode 100644 setec-web/templates/fail2ban.html create mode 100644 setec-web/templates/files.html create mode 100644 setec-web/templates/firewall.html create mode 100644 setec-web/templates/frontpage.html create mode 100644 setec-web/templates/nginx.html create mode 100644 setec-web/templates/security.html create mode 100644 setec-web/templates/settings.html create mode 100644 setec-web/templates/smtp.html create mode 100644 setec-web/templates/terminal.html create mode 100644 setec-web/templates/wizard.html create mode 100644 setec-web/wsgi.py create mode 100644 updates/sec/debian12_0326.sec create mode 100644 updates/sec/debian13_0326.sec create mode 100644 updates/sec/ubuntu2204_0326.sec create mode 100644 updates/sec/ubuntu2210_0326.sec create mode 100644 updates/sec/ubuntu2404_0326.sec create mode 100644 updates/sec/ubuntu2410_0326.sec diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a71680 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# ── Not part of the manager ── +site/ +insurrect-us/ +projects/ +services/ +setec-mgr/ +setec-tools/ +setup-vps.sh +fix-remaining.sh +ssh_cmd.py + +# ── Resources (keep logo only) ── +resources/* +!resources/setec_labs_logo.svg + +# ── Claude Code config (contains approved commands with secrets) ── +.claude/ + +# ── Python ── +__pycache__/ +*.pyc +*.pyo +*.egg-info/ +dist/ +build/ +*.egg +venv/ +.venv/ +env/ + +# ── Config (contains user secrets) ── +config.json +*.key +*.pem + +# ── Logs ── +*.log + +# ── Compiled Go agent binary ── +setec-web/agent/setec-agent + +# ── Node.js ── +node_modules/ + +# ── IDE ── +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# ── OS ── +.DS_Store +Thumbs.db +desktop.ini diff --git a/manager.bat b/manager.bat new file mode 100644 index 0000000..8a2f414 --- /dev/null +++ b/manager.bat @@ -0,0 +1,11 @@ +@echo off +cd /d "%~dp0setec-web" +echo =================================== +echo SETEC LABS Manager v2.0 +echo http://localhost:5000 +echo =================================== +echo. +pip install flask paramiko requests >nul 2>&1 +start http://localhost:5000 +python app.py +pause diff --git a/reload.bat b/reload.bat new file mode 100644 index 0000000..9b5a75a --- /dev/null +++ b/reload.bat @@ -0,0 +1,7 @@ +@echo off +taskkill /f /im python.exe >nul 2>&1 +timeout /t 1 /nobreak >nul +cd /d "%~dp0setec-web" +start http://localhost:5000 +python app.py +pause diff --git a/resources/setec_labs_logo.svg b/resources/setec_labs_logo.svg new file mode 100644 index 0000000..6e50045 --- /dev/null +++ b/resources/setec_labs_logo.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +SETEC LABS + + + + +SECURITY · RESEARCH · EXPLOITATION +v4.2.0 +SL-01 + \ No newline at end of file diff --git a/setec-web/agent/go.mod b/setec-web/agent/go.mod new file mode 100644 index 0000000..8dc99b7 --- /dev/null +++ b/setec-web/agent/go.mod @@ -0,0 +1,3 @@ +module setec-agent + +go 1.21 diff --git a/setec-web/agent/main.go b/setec-web/agent/main.go new file mode 100644 index 0000000..0eac6d7 --- /dev/null +++ b/setec-web/agent/main.go @@ -0,0 +1,189 @@ +// setec-agent — E2E encrypted command executor for SETEC LABS Manager. +// +// Reads AES-256-GCM tunnel key from /etc/setec/tunnel.key (32 bytes, hex-encoded). +// Receives base64-encoded encrypted commands on stdin. +// Decrypts, executes via /bin/sh, encrypts the response, outputs base64. +// +// Protocol: +// Input: base64( nonce[12] + AES-GCM-ciphertext ) +// Output: base64( nonce[12] + AES-GCM-ciphertext ) +// Plaintext response is JSON: {"stdout":"...","stderr":"...","exit_code":N} +// +// Build: GOOS=linux GOARCH=amd64 go build -o setec-agent -ldflags="-s -w" . + +package main + +import ( + "bufio" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "os/exec" + "strings" + "syscall" + "time" +) + +const keyPath = "/etc/setec/tunnel.key" + +type Response struct { + Stdout string `json:"stdout"` + Stderr string `json:"stderr"` + ExitCode int `json:"exit_code"` +} + +func loadKey() ([]byte, error) { + data, err := os.ReadFile(keyPath) + if err != nil { + return nil, fmt.Errorf("cannot read tunnel key: %w", err) + } + hexKey := strings.TrimSpace(string(data)) + key, err := hex.DecodeString(hexKey) + if err != nil { + return nil, fmt.Errorf("invalid tunnel key hex: %w", err) + } + if len(key) != 32 { + return nil, fmt.Errorf("tunnel key must be 32 bytes, got %d", len(key)) + } + return key, nil +} + +func decrypt(key []byte, b64input string) ([]byte, error) { + raw, err := base64.StdEncoding.DecodeString(strings.TrimSpace(b64input)) + if err != nil { + return nil, fmt.Errorf("base64 decode: %w", err) + } + if len(raw) < 12+16 { + return nil, fmt.Errorf("ciphertext too short") + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce := raw[:12] + ciphertext := raw[12:] + plaintext, err := gcm.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, fmt.Errorf("decryption failed (bad key or tampered data): %w", err) + } + return plaintext, nil +} + +func encrypt(key []byte, plaintext []byte) (string, error) { + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return "", err + } + + nonce := make([]byte, 12) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + + ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +func executeCommand(cmd string) Response { + ctx_timeout := 120 * time.Second + command := exec.Command("/bin/sh", "-c", cmd) + + var stdout, stderr strings.Builder + command.Stdout = &stdout + command.Stderr = &stderr + + // Start with timeout + done := make(chan error, 1) + command.Start() + go func() { done <- command.Wait() }() + + select { + case err := <-done: + exitCode := 0 + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { + exitCode = status.ExitStatus() + } else { + exitCode = 1 + } + } else { + exitCode = -1 + stderr.WriteString("\n" + err.Error()) + } + } + return Response{ + Stdout: stdout.String(), + Stderr: stderr.String(), + ExitCode: exitCode, + } + case <-time.After(ctx_timeout): + command.Process.Kill() + return Response{ + Stdout: stdout.String(), + Stderr: "command timed out after 120s", + ExitCode: -1, + } + } +} + +func main() { + key, err := loadKey() + if err != nil { + fmt.Fprintf(os.Stderr, "setec-agent: %s\n", err) + os.Exit(1) + } + + // Read one line from stdin (base64 encrypted command) + scanner := bufio.NewScanner(os.Stdin) + scanner.Buffer(make([]byte, 4*1024*1024), 4*1024*1024) // 4MB buffer + if !scanner.Scan() { + fmt.Fprintf(os.Stderr, "setec-agent: no input\n") + os.Exit(1) + } + + input := scanner.Text() + + // Decrypt command + cmdBytes, err := decrypt(key, input) + if err != nil { + fmt.Fprintf(os.Stderr, "setec-agent: decrypt error: %s\n", err) + os.Exit(1) + } + + // Execute + resp := executeCommand(string(cmdBytes)) + + // Marshal response + respJSON, err := json.Marshal(resp) + if err != nil { + fmt.Fprintf(os.Stderr, "setec-agent: json error: %s\n", err) + os.Exit(1) + } + + // Encrypt response + encResp, err := encrypt(key, respJSON) + if err != nil { + fmt.Fprintf(os.Stderr, "setec-agent: encrypt error: %s\n", err) + os.Exit(1) + } + + // Output + fmt.Println(encResp) +} diff --git a/setec-web/aide.py b/setec-web/aide.py new file mode 100644 index 0000000..593e03c --- /dev/null +++ b/setec-web/aide.py @@ -0,0 +1,141 @@ +""" +Command-builder module for managing AIDE (Advanced Intrusion Detection Environment) +file integrity monitoring on a Linux VPS. Each function returns a bash command string. +""" + + +def status_cmd() -> str: + """Check if AIDE is installed, show version and database file dates.""" + return ( + "echo '=== AIDE Status ===';" + " if command -v aide >/dev/null 2>&1; then" + " echo 'AIDE is installed';" + " aide --version 2>&1 | head -1;" + " else" + " echo 'AIDE is NOT installed';" + " fi;" + " echo;" + " echo '=== Database Files ===';" + " ls -lh /var/lib/aide/aide.db /var/lib/aide/aide.db.new 2>/dev/null" + " || echo 'No AIDE database files found'" + ) + + +def install_cmd() -> str: + """Install AIDE, initialize the database, and copy it into place.""" + return ( + "export DEBIAN_FRONTEND=noninteractive;" + " apt-get update -qq" + " && apt-get install -y -qq aide" + " && echo 'Running aideinit (this may take a while)...'" + " && aideinit" + " && cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db" + " && echo 'AIDE installed and database initialized successfully'" + ) + + +def check_cmd() -> str: + """Run AIDE integrity check showing changed, added, and removed files.""" + return ( + "echo '=== AIDE Integrity Check ===';" + " aide --check 2>&1;" + " echo;" + " echo 'Exit code:' $?" + ) + + +def update_cmd() -> str: + """Update AIDE database, accepting current filesystem state as the new baseline.""" + return ( + "echo '=== AIDE Database Update ===';" + " aide --update" + " && cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db" + " && echo 'Database updated — current state is now the baseline'" + ) + + +def init_cmd() -> str: + """Re-initialize the AIDE database from scratch.""" + return ( + "echo '=== AIDE Database Re-initialization ===';" + " aideinit" + " && cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db" + " && echo 'Database re-initialized successfully'" + ) + + +def log_cmd(lines: int = 50) -> str: + """Show the AIDE log file.""" + return ( + f"if [ -f /var/log/aide/aide.log ]; then" + f" tail -n {lines} /var/log/aide/aide.log;" + f" else" + f" echo 'No AIDE log found at /var/log/aide/aide.log';" + f" fi" + ) + + +def config_cmd() -> str: + """Display the full AIDE configuration file.""" + return "cat /etc/aide/aide.conf" + + +def config_rules_cmd() -> str: + """Show just the rule definitions from aide.conf (lines starting with / or =).""" + return ( + "echo '=== AIDE Rule Definitions ===';" + " grep -E '^(/|!|=)' /etc/aide/aide.conf" + ) + + +def compare_cmd() -> str: + """Compare two AIDE databases (current baseline vs new).""" + return ( + "echo '=== AIDE Database Comparison ===';" + " aide --compare 2>&1" + ) + + +def schedule_cmd(schedule: str = "daily") -> str: + """Set up a cron job for periodic AIDE checks (daily or weekly).""" + cron_script = "/etc/cron.{schedule}/aide-check".format(schedule=schedule) + script_body = ( + "#!/bin/bash\\n" + "/usr/bin/aide --check > /var/log/aide/aide.log 2>&1" + ) + return ( + f"echo -e '{script_body}' > {cron_script}" + f" && chmod 755 {cron_script}" + f" && echo 'AIDE {schedule} check scheduled at {cron_script}'" + ) + + +def schedule_status_cmd() -> str: + """Show any existing AIDE cron jobs.""" + return ( + "echo '=== AIDE Scheduled Jobs ===';" + " ls -la /etc/cron.daily/aide-check /etc/cron.weekly/aide-check 2>/dev/null" + " || echo 'No AIDE cron jobs found';" + " echo;" + " echo '=== Crontab entries ===';" + " crontab -l 2>/dev/null | grep -i aide" + " || echo 'No AIDE entries in crontab'" + ) + + +def schedule_remove_cmd() -> str: + """Remove all AIDE cron jobs.""" + return ( + "rm -f /etc/cron.daily/aide-check /etc/cron.weekly/aide-check" + " && echo 'AIDE scheduled checks removed'" + ) + + +def uninstall_cmd() -> str: + """Remove AIDE and its databases.""" + return ( + "export DEBIAN_FRONTEND=noninteractive;" + " apt-get remove --purge -y -qq aide" + " && rm -rf /var/lib/aide /var/log/aide" + " && echo 'AIDE uninstalled and data removed'" + ) diff --git a/setec-web/app.py b/setec-web/app.py new file mode 100644 index 0000000..f96ebdc --- /dev/null +++ b/setec-web/app.py @@ -0,0 +1,2724 @@ +from flask import Flask, render_template, request, jsonify, redirect, url_for +import os +import ssh_client +import dns_client +import config +import detector +import docker_store +import hardening +import security_apps +import ssl_audit +import monitoring +import ddos +import backup +import sec_updates +import clamav +import rkhunter +import chkrootkit +import lynis +import ossec +import modsecurity +import aide +import cowrie +import iptables +import nftables +import firewalld +import csf +import hosting +import audit +import sanitize +import traceback +import secrets + +app = Flask(__name__) +cfg_init = config.load() +app.secret_key = cfg_init.get("flask_secret", secrets.token_hex(32)) +app.config["SESSION_COOKIE_HTTPONLY"] = True +app.config["SESSION_COOKIE_SAMESITE"] = "Lax" + + +# ── Helpers ────────────────────────────────────────────────────────── +def ssh_run(cmd, timeout=30): + """Run command via SSH (uses E2E when enabled).""" + try: + return ssh_client.run(cmd, timeout=timeout) + except Exception as e: + return {"stdout": "", "stderr": str(e), "exit_code": -1} + + +def ssh_run_plain(cmd, timeout=30): + """Run command via plain SSH (bypasses E2E — for agent deployment only).""" + try: + return ssh_client.run_plain_always(cmd, timeout=timeout) + except Exception as e: + return {"stdout": "", "stderr": str(e), "exit_code": -1} + + +def api_wrap(fn): + """Wrap an API call, return JSON with error handling.""" + try: + result = fn() + return jsonify({"ok": True, "data": result}) + except Exception as e: + return jsonify({"ok": False, "error": str(e), "trace": traceback.format_exc()}), 500 + + +@app.after_request +def set_security_headers(response): + response.headers["X-Content-Type-Options"] = "nosniff" + response.headers["X-Frame-Options"] = "DENY" + response.headers["X-XSS-Protection"] = "1; mode=block" + response.headers["Referrer-Policy"] = "same-origin" + return response + + +@app.route("/api/wizard/accept-tos", methods=["POST"]) +def api_wizard_accept_tos(): + """Mark TOS as accepted — this is what unlocks the rest of the app.""" + cfg = config.load() + cfg["tos_accepted"] = True + config.save(cfg) + audit.log("tos_accepted", ip=request.remote_addr) + return jsonify({"ok": True}) + + +@app.route("/api/audit/log") +def api_audit_log(): + count = request.args.get("count", 100, type=int) + return jsonify({"ok": True, "data": audit.get_recent(count)}) + + +# ── API: E2E Tunnel ────────────────────────────────────────────────── +@app.route("/api/e2e/status") +def api_e2e_status(): + """Check E2E tunnel status.""" + import e2e + cfg = config.load() + return jsonify({"ok": True, "data": { + "e2e_enabled": cfg.get("e2e_enabled", False), + "e2e_deployed": cfg.get("e2e_tunnel_key_deployed", False), + "agent_path": e2e.AGENT_PATH, + }}) + + +@app.route("/api/e2e/toggle", methods=["POST"]) +def api_e2e_toggle(): + """Enable or disable E2E tunnel encryption.""" + import e2e + data = request.json or {} + enabled = data.get("enabled") + + cfg = config.load() + + if enabled and not cfg.get("e2e_tunnel_key_deployed", False): + return jsonify({"ok": False, "error": "Deploy the E2E agent first before enabling."}), 400 + + if enabled and not cfg.get("tunnel_key", ""): + return jsonify({"ok": False, "error": "No tunnel key configured."}), 400 + + cfg["e2e_enabled"] = bool(enabled) + config.save(cfg) + state = "enabled" if enabled else "disabled" + audit.log("e2e_toggled", ip=request.remote_addr, details=f"E2E tunnel {state}") + return jsonify({"ok": True, "data": {"e2e_enabled": cfg["e2e_enabled"]}}) + + +@app.route("/api/e2e/deploy", methods=["POST"]) +def api_e2e_deploy(): + """Deploy the E2E agent and tunnel key to the VPS.""" + import e2e + import subprocess + + cfg = config.load() + + # Derive tunnel key or generate a new one + tunnel_key_hex = cfg.get("tunnel_key", "") + if not tunnel_key_hex: + # Generate tunnel key from password-based derivation + # The tunnel key is stored in config for future use + import os + tunnel_key = os.urandom(32) + tunnel_key_hex = tunnel_key.hex() + cfg["tunnel_key"] = tunnel_key_hex + config.save(cfg) + else: + tunnel_key = bytes.fromhex(tunnel_key_hex) + + results = [] + + # Step 1: Deploy tunnel key + deploy_cmds = e2e.generate_deploy_commands(tunnel_key) + for desc, cmd in deploy_cmds: + res = ssh_run_plain(cmd, timeout=10) + results.append({"step": desc, "ok": res["exit_code"] == 0, + "output": res["stdout"] + res["stderr"]}) + if res["exit_code"] != 0: + return jsonify({"ok": False, "error": f"Failed at: {desc}", "data": results}) + + # Step 2: Upload the Go agent binary + # Cross-compile if possible, otherwise upload from pre-built + agent_dir = os.path.join(os.path.dirname(__file__), "agent") + agent_binary = os.path.join(agent_dir, "setec-agent") + + # Try to cross-compile + try: + env = dict(os.environ) + env["GOOS"] = "linux" + env["GOARCH"] = "amd64" + env["CGO_ENABLED"] = "0" + compile_result = subprocess.run( + ["go", "build", "-o", agent_binary, "-ldflags=-s -w", "."], + cwd=agent_dir, capture_output=True, text=True, timeout=60, env=env + ) + if compile_result.returncode != 0: + results.append({"step": "Cross-compile agent", "ok": False, + "output": compile_result.stderr}) + return jsonify({"ok": False, "error": "Go cross-compile failed. Install Go or pre-build the agent.", + "data": results}) + results.append({"step": "Cross-compile agent", "ok": True, "output": "Built setec-agent for linux/amd64"}) + except FileNotFoundError: + return jsonify({"ok": False, "error": "Go not found. Install Go to cross-compile the E2E agent.", + "data": results}) + except Exception as ex: + return jsonify({"ok": False, "error": f"Compile error: {str(ex)}", "data": results}) + + # Step 3: Upload agent via SCP + try: + key_path = cfg["ssh_key_path"] + host = cfg["vps_host"] + user = cfg["vps_user"] + port = cfg["vps_port"] + + scp_cmd = ( + f'scp -i "{key_path}" -o StrictHostKeyChecking=no -P {port} ' + f'"{agent_binary}" {user}@{host}:{e2e.AGENT_PATH}' + ) + scp_result = subprocess.run(scp_cmd, shell=True, capture_output=True, text=True, timeout=30) + if scp_result.returncode != 0: + results.append({"step": "Upload agent", "ok": False, "output": scp_result.stderr}) + return jsonify({"ok": False, "error": "SCP upload failed", "data": results}) + results.append({"step": "Upload agent", "ok": True, "output": "Uploaded to " + e2e.AGENT_PATH}) + except Exception as ex: + return jsonify({"ok": False, "error": f"Upload error: {str(ex)}", "data": results}) + + # Step 4: Set permissions on the agent + res = ssh_run_plain(f"chmod 755 {e2e.AGENT_PATH} && {e2e.AGENT_PATH} --help 2>&1 || echo 'agent installed'", timeout=10) + results.append({"step": "Set agent permissions", "ok": True, "output": res["stdout"]}) + + # Mark E2E as deployed + cfg["e2e_tunnel_key_deployed"] = True + config.save(cfg) + + audit.log("e2e_deployed", ip=request.remote_addr, details="Agent + tunnel key deployed to VPS") + return jsonify({"ok": True, "data": results}) + + +@app.route("/api/e2e/test", methods=["POST"]) +def api_e2e_test(): + """Test E2E encrypted command execution.""" + import e2e + if not e2e.is_e2e_enabled(): + return jsonify({"ok": False, "error": "E2E not deployed yet"}) + + # Send an encrypted test command + result = ssh_run("echo 'E2E tunnel active' && date && whoami", timeout=15) + if result["exit_code"] == 0 and "E2E tunnel active" in result["stdout"]: + return jsonify({"ok": True, "data": { + "message": "E2E tunnel working", + "output": result["stdout"], + }}) + else: + return jsonify({"ok": False, "error": "E2E test failed", + "data": result}) + + +# ── Pages ──────────────────────────────────────────────────────────── +@app.route("/") +def dashboard(): + cfg = config.load() + if not cfg.get("tos_accepted", False): + return redirect(url_for("wizard_page")) + return render_template("dashboard.html") + + +@app.route("/docker") +def docker_page(): + return render_template("docker.html") + + +@app.route("/dns") +def dns_page(): + return render_template("dns.html") + + +@app.route("/nginx") +def nginx_page(): + return render_template("nginx.html") + + +@app.route("/smtp") +def smtp_page(): + return render_template("smtp.html") + + +@app.route("/files") +def files_page(): + return render_template("files.html") + + +@app.route("/terminal") +def terminal_page(): + return render_template("terminal.html") + + +@app.route("/configs") +def configs_page(): + return render_template("configs.html") + + +@app.route("/detect") +def detect_page(): + return render_template("detect.html") + + +@app.route("/fail2ban") +def fail2ban_page(): + return render_template("fail2ban.html") + + +@app.route("/frontpage") +def frontpage_page(): + return render_template("frontpage.html") + + +@app.route("/settings") +def settings_page(): + return render_template("settings.html") + + +@app.route("/firewall") +def firewall_page(): + return render_template("firewall.html") + + +@app.route("/security") +def security_page(): + return render_template("security.html") + + +@app.route("/wizard") +def wizard_page(): + return render_template("wizard.html") + + +@app.route("/docs") +def docs_page(): + return render_template("docs.html") + + +# ── API: Server Status ────────────────────────────────────────────── +@app.route("/api/status") +def api_status(): + return api_wrap(lambda: ssh_run( + "echo '=== HOSTNAME ===' && hostname && " + "echo '=== UPTIME ===' && uptime && " + "echo '=== MEMORY ===' && free -h && " + "echo '=== DISK ===' && df -h / && " + "echo '=== LOAD ===' && cat /proc/loadavg && " + "echo '=== DOCKER ===' && docker ps --format '{{.Names}}: {{.Status}}' 2>/dev/null && " + "echo '=== NGINX ===' && systemctl is-active nginx && " + "echo '=== POSTFIX ===' && systemctl is-active postfix 2>/dev/null" + )) + + +@app.route("/api/status/domains") +def api_domain_status(): + cfg = config.load() + domain = cfg["domain"] + subs = ["", "repo.", "git.", "app.", "files.", "lists."] + cmd = " && ".join( + f"echo '{s}{domain}: '$(curl -s -o /dev/null -w '%{{http_code}}' --max-time 5 https://{s}{domain})" + for s in subs + ) + return api_wrap(lambda: ssh_run(cmd)) + + +# ── API: Docker ────────────────────────────────────────────────────── +@app.route("/api/docker/list") +def api_docker_list(): + return api_wrap(lambda: ssh_run( + "docker ps -a --format '{{.ID}}|{{.Names}}|{{.Image}}|{{.Status}}|{{.Ports}}'" + )) + + +@app.route("/api/docker//", methods=["POST"]) +def api_docker_action(action, name): + if action not in ("start", "stop", "restart"): + return jsonify({"ok": False, "error": "invalid action"}), 400 + return api_wrap(lambda: ssh_run(f"docker {action} {name} 2>&1")) + + +@app.route("/api/docker/logs/") +def api_docker_logs(name): + lines = request.args.get("lines", 50, type=int) + return api_wrap(lambda: ssh_run(f"docker logs --tail {lines} {name} 2>&1")) + + +@app.route("/api/docker/compose/", methods=["POST"]) +def api_docker_compose(action): + cfg = config.load() + compose_dir = cfg["compose_path"].rsplit("/", 1)[0] + if action == "up": + cmd = f"cd {compose_dir} && docker compose up -d 2>&1" + elif action == "down": + cmd = f"cd {compose_dir} && docker compose down 2>&1" + elif action == "pull": + cmd = f"cd {compose_dir} && docker compose pull 2>&1" + else: + return jsonify({"ok": False, "error": "invalid action"}), 400 + return api_wrap(lambda: ssh_run(cmd, timeout=120)) + + +# ── API: Docker Store ──────────────────────────────────────────────── +@app.route("/api/docker/store") +def api_docker_store(): + return jsonify({"ok": True, "data": docker_store.STORE, "categories": docker_store.CATEGORIES}) + + +@app.route("/api/docker/store/install", methods=["POST"]) +def api_docker_store_install(): + """Install an app from the store by appending to docker-compose.yml and running up.""" + data = request.json + name = data.get("name", "") + app_entry = next((a for a in docker_store.STORE if a["name"] == name), None) + if not app_entry: + return jsonify({"ok": False, "error": f"App '{name}' not found in store"}), 400 + + cfg = config.load() + compose_path = cfg["compose_path"] + snippet = app_entry["compose"] + + # Append the service to docker-compose.yml, then run up + cmd = ( + f"echo '' >> {compose_path} && " + f"cat >> {compose_path} << 'STOREEOF'\n{snippet}\nSTOREEOF\n" + f"cd {compose_path.rsplit('/', 1)[0]} && docker compose up -d 2>&1" + ) + return api_wrap(lambda: ssh_run(cmd, timeout=120)) + + +@app.route("/api/docker/install-git", methods=["POST"]) +def api_docker_install_git(): + """Clone a git repo and run docker compose up from it.""" + data = request.json + repo = data.get("repo", "") + name = data.get("name", "") + if not repo: + return jsonify({"ok": False, "error": "no repo URL"}), 400 + if not name: + # Extract name from repo URL + name = repo.rstrip("/").split("/")[-1].replace(".git", "") + + install_dir = f"/opt/seteclabs/{name}" + cmd = ( + f"apt-get install -y git > /dev/null 2>&1; " + f"git clone '{repo}' '{install_dir}' 2>&1 && " + f"cd '{install_dir}' && " + f"if [ -f docker-compose.yml ] || [ -f docker-compose.yaml ] || [ -f compose.yml ] || [ -f compose.yaml ]; then " + f" docker compose up -d 2>&1; " + f"else " + f" echo 'No docker-compose file found in repo. Contents:' && ls -la; " + f"fi" + ) + return api_wrap(lambda: ssh_run(cmd, timeout=120)) + + +@app.route("/api/docker/install-url", methods=["POST"]) +def api_docker_install_url(): + """Download from a URL and run/install it.""" + data = request.json + url = data.get("url", "") + name = data.get("name", "") + if not url or not name: + return jsonify({"ok": False, "error": "need url and name"}), 400 + + install_dir = f"/opt/seteclabs/{name}" + filename = url.split("/")[-1].split("?")[0] + + cmd = ( + f"mkdir -p '{install_dir}' && cd '{install_dir}' && " + f"curl -fSL -o '{filename}' '{url}' 2>&1 && " + f"echo 'Downloaded: {filename}' && " + f"ls -lh '{filename}' && " + f"if echo '{filename}' | grep -qE '\\.(tar\\.gz|tgz)$'; then " + f" tar xzf '{filename}' 2>&1 && echo 'Extracted tar.gz'; " + f"elif echo '{filename}' | grep -qE '\\.zip$'; then " + f" unzip -o '{filename}' 2>&1 && echo 'Extracted zip'; " + f"elif echo '{filename}' | grep -qE '\\.tar$'; then " + f" tar xf '{filename}' 2>&1 && echo 'Extracted tar'; " + f"fi && " + f"if [ -f docker-compose.yml ] || [ -f docker-compose.yaml ] || [ -f compose.yml ]; then " + f" docker compose up -d 2>&1; " + f"else " + f" echo 'Contents:' && ls -la; " + f"fi" + ) + return api_wrap(lambda: ssh_run(cmd, timeout=120)) + + +@app.route("/api/docker/install-compose", methods=["POST"]) +def api_docker_install_compose(): + """Install from a raw docker-compose snippet.""" + data = request.json + name = data.get("name", "") + compose_content = data.get("compose", "") + if not name or not compose_content: + return jsonify({"ok": False, "error": "need name and compose content"}), 400 + + install_dir = f"/opt/seteclabs/{name}" + cmd = ( + f"mkdir -p '{install_dir}' && " + f"cat > '{install_dir}/docker-compose.yml' << 'COMPEOF'\n{compose_content}\nCOMPEOF\n" + f"cd '{install_dir}' && docker compose up -d 2>&1" + ) + return api_wrap(lambda: ssh_run(cmd, timeout=120)) + + +# ── API: DNS ───────────────────────────────────────────────────────── +@app.route("/api/dns/records") +def api_dns_records(): + return api_wrap(dns_client.get_records) + + +@app.route("/api/dns/add", methods=["POST"]) +def api_dns_add(): + data = request.json + rtype = data.get("type", "A") + name = data.get("name", "") + value = data.get("value", "") + if rtype == "A": + return api_wrap(lambda: dns_client.add_a_record(name, value)) + elif rtype == "TXT": + return api_wrap(lambda: dns_client.add_txt_record(name, value)) + return jsonify({"ok": False, "error": "unsupported type"}), 400 + + +@app.route("/api/dns/delete/", methods=["DELETE"]) +def api_dns_delete(record_id): + return api_wrap(lambda: dns_client.delete_record(record_id)) + + +# ── API: Nginx ─────────────────────────────────────────────────────── +@app.route("/api/nginx/sites") +def api_nginx_sites(): + return api_wrap(lambda: ssh_run( + "ls -1 /etc/nginx/sites-enabled/ 2>/dev/null && echo '---' && nginx -T 2>/dev/null | grep server_name" + )) + + +@app.route("/api/nginx/config/") +def api_nginx_config(site): + return api_wrap(lambda: ssh_run(f"cat /etc/nginx/sites-available/{site} 2>/dev/null")) + + +@app.route("/api/nginx/add-subdomain", methods=["POST"]) +def api_nginx_add_subdomain(): + data = request.json + subdomain = data.get("subdomain", "") + proxy_port = data.get("proxy_port") + static_root = data.get("static_root") + cfg = config.load() + fqdn = f"{subdomain}.{cfg['domain']}" if subdomain else cfg["domain"] + + if proxy_port: + block = f"""server {{ + listen 80; + server_name {fqdn}; + location / {{ + proxy_pass http://127.0.0.1:{proxy_port}; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + }} +}}""" + elif static_root: + block = f"""server {{ + listen 80; + server_name {fqdn}; + root {static_root}; + index index.html; + location / {{ + try_files $uri $uri/ =404; + }} +}}""" + else: + return jsonify({"ok": False, "error": "need proxy_port or static_root"}), 400 + + escaped = block.replace("'", "'\\''") + cmd = ( + f"echo '{escaped}' > /etc/nginx/sites-available/{fqdn} && " + f"ln -sf /etc/nginx/sites-available/{fqdn} /etc/nginx/sites-enabled/{fqdn} && " + f"nginx -t 2>&1 && systemctl reload nginx 2>&1 && " + f"echo 'Site {fqdn} added and nginx reloaded'" + ) + return api_wrap(lambda: ssh_run(cmd)) + + +@app.route("/api/nginx/ssl/", methods=["POST"]) +def api_nginx_ssl(site): + return api_wrap(lambda: ssh_run( + f"certbot --nginx -d {site} --non-interactive --agree-tos -m admin@{config.load()['domain']} 2>&1", + timeout=60, + )) + + +@app.route("/api/nginx/reload", methods=["POST"]) +def api_nginx_reload(): + return api_wrap(lambda: ssh_run("nginx -t 2>&1 && systemctl reload nginx 2>&1")) + + +# ── API: SMTP ──────────────────────────────────────────────────────── +@app.route("/api/smtp/status") +def api_smtp_status(): + return api_wrap(lambda: ssh_run( + "echo '=== Postfix ===' && systemctl is-active postfix && " + "echo '=== Queue ===' && mailq | tail -5 && " + "echo '=== DKIM ===' && systemctl is-active opendkim && " + "echo '=== Listmonk ===' && docker ps --filter name=listmonk --format '{{.Names}}: {{.Status}}' && " + "echo '=== Recent Log ===' && (journalctl -u postfix --no-pager -n 10 2>/dev/null || tail -10 /var/log/mail.log)" + )) + + +@app.route("/api/smtp/send-test", methods=["POST"]) +def api_smtp_send_test(): + to = request.json.get("to", "") + domain = config.load()["domain"] + return api_wrap(lambda: ssh_run( + f"echo 'SETEC LABS mail test - sent at $(date)' | mail -s 'SETEC LABS - SMTP Test' -r noreply@{domain} '{to}' 2>&1 && echo 'Test email sent to {to}'" + )) + + +@app.route("/api/smtp/flush", methods=["POST"]) +def api_smtp_flush(): + return api_wrap(lambda: ssh_run("postfix flush 2>&1 && echo 'Queue flushed'")) + + +@app.route("/api/smtp/restart", methods=["POST"]) +def api_smtp_restart(): + return api_wrap(lambda: ssh_run("systemctl restart postfix opendkim 2>&1 && echo 'Postfix + DKIM restarted'")) + + +@app.route("/api/smtp/dns-check") +def api_smtp_dns(): + domain = config.load()["domain"] + return api_wrap(lambda: ssh_run( + f"echo '=== SPF ===' && dig +short TXT {domain} | grep spf && " + f"echo '=== DKIM ===' && dig +short TXT setec._domainkey.{domain} && " + f"echo '=== DMARC ===' && dig +short TXT _dmarc.{domain} && " + f"echo '=== MX ===' && dig +short MX {domain}" + )) + + +# ── API: SMTP Send ────────────────────────────────────────────────── +@app.route("/api/smtp/send", methods=["POST"]) +def api_smtp_send(): + data = request.json + from_addr = data.get("from", "noreply@seteclabs.io") + to = data.get("to", "") + subject = data.get("subject", "") + body = data.get("body", "") + if not to or not subject: + return jsonify({"ok": False, "error": "need to and subject"}), 400 + cmd = ( + f"echo '{body}' | mail -s '{subject}' -r '{from_addr}' '{to}' 2>&1 && " + f"echo 'Email sent to {to}'" + ) + return api_wrap(lambda: ssh_run(cmd)) + + +@app.route("/api/smtp/send-mass", methods=["POST"]) +def api_smtp_send_mass(): + """Send individual emails to each recipient (BCC mode - no one sees others).""" + data = request.json + from_addr = data.get("from", "noreply@seteclabs.io") + recipients = data.get("recipients", []) + subject = data.get("subject", "") + body = data.get("body", "") + if not recipients or not subject: + return jsonify({"ok": False, "error": "need recipients and subject"}), 400 + + # Build a script that sends one email per recipient + lines = ["#!/bin/bash", "SENT=0", "FAILED=0"] + for addr in recipients: + addr = addr.strip().replace("'", "") + if not addr or "@" not in addr: + continue + lines.append( + f"echo '{body}' | mail -s '{subject}' -r '{from_addr}' '{addr}' 2>/dev/null " + f"&& SENT=$((SENT+1)) || FAILED=$((FAILED+1))" + ) + lines.append('echo "Sent: $SENT | Failed: $FAILED | Total: $((SENT+FAILED))"') + script = "\n".join(lines) + + cmd = f"bash << 'MASSEOF'\n{script}\nMASSEOF" + return api_wrap(lambda: ssh_run(cmd, timeout=min(len(recipients) * 3 + 10, 120))) + + +# ── API: File Manager ─────────────────────────────────────────────── +@app.route("/api/files/list") +def api_files_list(): + path = request.args.get("path", "/var/www") + return api_wrap(lambda: ssh_run(f"ls -la {path} 2>&1")) + + +@app.route("/api/files/read") +def api_files_read(): + path = request.args.get("path", "") + return api_wrap(lambda: ssh_run(f"cat {path} 2>&1")) + + +@app.route("/api/files/write", methods=["POST"]) +def api_files_write(): + data = request.json + path = data.get("path", "") + content = data.get("content", "") + escaped = content.replace("\\", "\\\\").replace("'", "'\\''") + return api_wrap(lambda: ssh_run(f"cat > {path} << 'SETECEOF'\n{content}\nSETECEOF")) + + +@app.route("/api/files/mkdir", methods=["POST"]) +def api_files_mkdir(): + path = request.json.get("path", "") + return api_wrap(lambda: ssh_run(f"mkdir -p {path} 2>&1 && echo 'Created {path}'")) + + +@app.route("/api/files/delete", methods=["DELETE"]) +def api_files_delete(): + path = request.json.get("path", "") + return api_wrap(lambda: ssh_run(f"rm -rf {path} 2>&1 && echo 'Deleted {path}'")) + + +# ── API: Deploy ────────────────────────────────────────────────────── +@app.route("/api/deploy/site", methods=["POST"]) +def api_deploy_site(): + """Deploy local site/ folder to VPS via tar over SSH.""" + import subprocess, os + cfg = config.load() + site_dir = os.path.join(os.path.dirname(__file__), "..", "site") + site_dir = os.path.abspath(site_dir) + if not os.path.isdir(site_dir): + return jsonify({"ok": False, "error": f"site/ directory not found at {site_dir}"}), 400 + + key = cfg["ssh_key_path"] + host = cfg["vps_host"] + user = cfg["vps_user"] + port = cfg["vps_port"] + web_root = f"{cfg['web_root']}/{cfg['domain']}" + + try: + # tar the site, pipe over ssh, extract on server + cmd = ( + f'cd "{site_dir}" && tar cf - . | ' + f'ssh -i "{key}" -o StrictHostKeyChecking=no -p {port} {user}@{host} ' + f'"mkdir -p {web_root} && tar xf - -C {web_root}"' + ) + result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30) + return jsonify({ + "ok": result.returncode == 0, + "data": {"stdout": result.stdout, "stderr": result.stderr, "exit_code": result.returncode}, + }) + except Exception as e: + return jsonify({"ok": False, "error": str(e)}), 500 + + +# ── API: Front Page Editor ────────────────────────────────────────── +@app.route("/api/frontpage/list") +def api_frontpage_list(): + cfg = config.load() + web_root = f"{cfg['web_root']}/{cfg['domain']}" + return api_wrap(lambda: ssh_run( + f"find {web_root} -type f \\( -name '*.html' -o -name '*.css' -o -name '*.js' \\) " + f"-printf '%P|%s|%T@\\n' 2>/dev/null | sort" + )) + + +@app.route("/api/frontpage/read") +def api_frontpage_read(): + cfg = config.load() + web_root = f"{cfg['web_root']}/{cfg['domain']}" + filename = request.args.get("file", "") + if not filename or ".." in filename: + return jsonify({"ok": False, "error": "invalid file"}), 400 + path = f"{web_root}/{filename}" + return api_wrap(lambda: ssh_run(f"cat '{path}' 2>&1")) + + +@app.route("/api/frontpage/write", methods=["POST"]) +def api_frontpage_write(): + cfg = config.load() + web_root = f"{cfg['web_root']}/{cfg['domain']}" + data = request.json + filename = data.get("file", "") + content = data.get("content", "") + if not filename or ".." in filename: + return jsonify({"ok": False, "error": "invalid file"}), 400 + path = f"{web_root}/{filename}" + backup_cmd = f"cp '{path}' '{path}.bak.$(date +%Y%m%d_%H%M%S)' 2>/dev/null; " + write_cmd = f"cat > '{path}' << 'SETECEOF'\n{content}\nSETECEOF" + return api_wrap(lambda: ssh_run(backup_cmd + write_cmd)) + + +@app.route("/api/frontpage/new", methods=["POST"]) +def api_frontpage_new(): + cfg = config.load() + web_root = f"{cfg['web_root']}/{cfg['domain']}" + data = request.json + filename = data.get("file", "") + if not filename or ".." in filename: + return jsonify({"ok": False, "error": "invalid file"}), 400 + path = f"{web_root}/{filename}" + return api_wrap(lambda: ssh_run( + f"mkdir -p $(dirname '{path}') && touch '{path}' 2>&1 && echo 'Created {filename}'" + )) + + +@app.route("/api/frontpage/delete", methods=["DELETE"]) +def api_frontpage_delete(): + cfg = config.load() + web_root = f"{cfg['web_root']}/{cfg['domain']}" + filename = request.json.get("file", "") + if not filename or ".." in filename: + return jsonify({"ok": False, "error": "invalid file"}), 400 + path = f"{web_root}/{filename}" + return api_wrap(lambda: ssh_run(f"rm '{path}' 2>&1 && echo 'Deleted {filename}'")) + + +@app.route("/api/frontpage/preview-url") +def api_frontpage_preview(): + cfg = config.load() + return jsonify({"ok": True, "data": f"https://{cfg['domain']}"}) + + +# ── API: Terminal ──────────────────────────────────────────────────── +@app.route("/api/terminal/exec", methods=["POST"]) +def api_terminal_exec(): + cmd = request.json.get("cmd", "") + timeout = request.json.get("timeout", 30) + return api_wrap(lambda: ssh_run(cmd, timeout=min(timeout, 120))) + + +# ── API: Settings ──────────────────────────────────────────────────── +@app.route("/api/settings", methods=["GET"]) +def api_settings_get(): + return jsonify({"ok": True, "data": config.safe_config()}) + + +@app.route("/api/settings", methods=["POST"]) +def api_settings_save(): + data = request.json + cfg = config.load() + for k in ("vps_host", "vps_user", "vps_port", "ssh_key_path", "domain", + "web_root", "compose_path", "flask_port", "hosting_provider"): + if k in data: + cfg[k] = data[k] + if data.get("hostinger_api_key") and "..." not in data["hostinger_api_key"]: + cfg["hostinger_api_key"] = data["hostinger_api_key"] + # Mark setup complete when saving from wizard + if data.get("setup_complete"): + cfg["setup_complete"] = True + config.save(cfg) + ssh_client.close() # reconnect with new settings + audit.log("settings_changed", ip=request.remote_addr, + details=", ".join(data.keys())) + return jsonify({"ok": True}) + + +@app.route("/api/ssh/test") +def api_ssh_test(): + return api_wrap(lambda: ssh_run("echo 'SSH connection OK' && hostname && whoami")) + + +# ── API: Fail2Ban ──────────────────────────────────────────────────── +@app.route("/api/fail2ban/status") +def api_f2b_status(): + return api_wrap(lambda: ssh_run("fail2ban-client status 2>&1")) + + +@app.route("/api/fail2ban/jail/") +def api_f2b_jail(name): + return api_wrap(lambda: ssh_run(f"fail2ban-client status {name} 2>&1")) + + +@app.route("/api/fail2ban/ban", methods=["POST"]) +def api_f2b_ban(): + data = request.json + jail = data.get("jail", "sshd") + ip = data.get("ip", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(f"fail2ban-client set {jail} banip {ip} 2>&1")) + + +@app.route("/api/fail2ban/unban", methods=["POST"]) +def api_f2b_unban(): + data = request.json + jail = data.get("jail", "sshd") + ip = data.get("ip", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(f"fail2ban-client set {jail} unbanip {ip} 2>&1")) + + +@app.route("/api/fail2ban/unban-all", methods=["POST"]) +def api_f2b_unban_all(): + return api_wrap(lambda: ssh_run("fail2ban-client unban --all 2>&1")) + + +@app.route("/api/fail2ban/jails") +def api_f2b_jails(): + return api_wrap(lambda: ssh_run( + "for j in $(fail2ban-client status | grep 'Jail list' | sed 's/.*://;s/,//g'); do " + "echo \"=== $j ===\"; fail2ban-client status $j 2>&1; echo; done" + )) + + +@app.route("/api/fail2ban/config") +def api_f2b_config(): + return api_wrap(lambda: ssh_run("cat /etc/fail2ban/jail.local 2>&1")) + + +@app.route("/api/fail2ban/config/save", methods=["POST"]) +def api_f2b_config_save(): + content = request.json.get("content", "") + cmd = ( + "cp /etc/fail2ban/jail.local /etc/fail2ban/jail.local.bak.$(date +%Y%m%d_%H%M%S) 2>/dev/null; " + f"cat > /etc/fail2ban/jail.local << 'F2BEOF'\n{content}\nF2BEOF\n" + "fail2ban-client reload 2>&1 && echo 'Config saved and fail2ban reloaded'" + ) + return api_wrap(lambda: ssh_run(cmd)) + + +@app.route("/api/fail2ban/reload", methods=["POST"]) +def api_f2b_reload(): + return api_wrap(lambda: ssh_run("fail2ban-client reload 2>&1 && echo 'Reloaded'")) + + +@app.route("/api/fail2ban/log") +def api_f2b_log(): + lines = request.args.get("lines", 50, type=int) + return api_wrap(lambda: ssh_run(f"tail -{lines} /var/log/fail2ban.log 2>&1")) + + +# ── API: Service Detection ────────────────────────────────────────── +@app.route("/api/detect/scan") +def api_detect_scan(): + cmd = detector.build_detect_command() + result = ssh_run(cmd, timeout=30) + if result["exit_code"] not in (0, -1) and not result["stdout"]: + return jsonify({"ok": False, "error": result["stderr"]}), 500 + detected = detector.parse_detection(result["stdout"]) + return jsonify({"ok": True, "data": detected}) + + +@app.route("/api/detect/all-services") +def api_detect_all(): + """Return the full service database for browsing.""" + by_cat = {} + for svc in detector.SERVICES: + cat = detector.CATEGORIES.get(svc["cat"], svc["cat"]) + by_cat.setdefault(cat, []).append({ + "name": svc["name"], + "ports": svc["ports"], + "configs": svc["configs"], + "packages": svc["pkg"], + }) + return jsonify({"ok": True, "data": by_cat}) + + +# ── API: Config Editor ────────────────────────────────────────────── +@app.route("/api/configs/read") +def api_configs_read(): + path = request.args.get("path", "") + if not path: + return jsonify({"ok": False, "error": "no path"}), 400 + return api_wrap(lambda: ssh_run(f"cat '{path}' 2>&1")) + + +@app.route("/api/configs/write", methods=["POST"]) +def api_configs_write(): + data = request.json + path = data.get("path", "") + content = data.get("content", "") + if not path: + return jsonify({"ok": False, "error": "no path"}), 400 + # Backup before writing + backup_cmd = f"cp '{path}' '{path}.bak.$(date +%Y%m%d_%H%M%S)' 2>/dev/null; " + write_cmd = f"cat > '{path}' << 'SETECEOF'\n{content}\nSETECEOF" + return api_wrap(lambda: ssh_run(backup_cmd + write_cmd)) + + +@app.route("/api/configs/test-nginx", methods=["POST"]) +def api_configs_test_nginx(): + return api_wrap(lambda: ssh_run("nginx -t 2>&1")) + + +@app.route("/api/configs/reload-service", methods=["POST"]) +def api_configs_reload_service(): + svc = request.json.get("service", "") + if not svc: + return jsonify({"ok": False, "error": "no service name"}), 400 + allowed = {"nginx", "postfix", "opendkim", "sshd", "fail2ban", "ufw", + "docker", "redis", "postgresql", "mysql", "grafana-server", + "prometheus", "dovecot", "unbound", "bind9", "haproxy", + "php8.1-fpm", "php8.2-fpm", "php8.3-fpm", "supervisor"} + if svc not in allowed: + return api_wrap(lambda: ssh_run(f"systemctl reload {svc} 2>&1 || systemctl restart {svc} 2>&1")) + return api_wrap(lambda: ssh_run(f"systemctl reload {svc} 2>&1 || systemctl restart {svc} 2>&1")) + + +@app.route("/api/configs/diff", methods=["POST"]) +def api_configs_diff(): + path = request.json.get("path", "") + return api_wrap(lambda: ssh_run( + f"ls -1t '{path}'.bak.* 2>/dev/null | head -1 | xargs -I{{}} diff '{{}}' '{path}' 2>&1 || echo 'No backup found'" + )) + + +@app.route("/api/configs/backups") +def api_configs_backups(): + path = request.args.get("path", "") + return api_wrap(lambda: ssh_run(f"ls -lt '{path}'.bak.* 2>/dev/null | head -10 || echo 'No backups'")) + + +@app.route("/api/configs/restore", methods=["POST"]) +def api_configs_restore(): + data = request.json + backup = data.get("backup", "") + target = data.get("target", "") + if not backup or not target: + return jsonify({"ok": False, "error": "need backup and target paths"}), 400 + return api_wrap(lambda: ssh_run(f"cp '{backup}' '{target}' 2>&1 && echo 'Restored {backup} -> {target}'")) + + +# ── API: Firewall Dashboard ────────────────────────────────────────── +@app.route("/api/firewall/detect") +def api_fw_detect(): + return api_wrap(lambda: ssh_run( + "echo '=== Firewall Detection ===' && " + "echo -n 'ufw: ' && (which ufw >/dev/null 2>&1 && ufw status | head -1 || echo 'not installed') && " + "echo -n 'iptables: ' && (which iptables >/dev/null 2>&1 && echo \"installed ($(iptables -L -n 2>/dev/null | grep -c '^Chain') chains)\" || echo 'not installed') && " + "echo -n 'nftables: ' && (which nft >/dev/null 2>&1 && echo \"installed ($(nft list tables 2>/dev/null | wc -l) tables)\" || echo 'not installed') && " + "echo -n 'firewalld: ' && (which firewall-cmd >/dev/null 2>&1 && firewall-cmd --state 2>/dev/null || echo 'not installed') && " + "echo -n 'csf: ' && (which csf >/dev/null 2>&1 && csf -v 2>/dev/null | head -1 || echo 'not installed') && " + "echo '' && echo '=== Default Firewall ===' && " + "if ufw status 2>/dev/null | grep -q 'Status: active'; then echo 'UFW is the active firewall'; " + "elif systemctl is-active firewalld >/dev/null 2>&1; then echo 'firewalld is active'; " + "elif systemctl is-active nftables >/dev/null 2>&1; then echo 'nftables service is active'; " + "elif csf -l >/dev/null 2>&1; then echo 'CSF is active'; " + "else echo 'iptables (raw) — no frontend active'; fi" + )) + + +@app.route("/api/firewall/ports") +def api_fw_ports(): + return api_wrap(lambda: ssh_run( + "echo '=== Listening Ports ===' && " + "ss -tlnp 2>&1 && " + "echo '' && echo '=== UDP Listeners ===' && " + "ss -ulnp 2>&1" + )) + + +@app.route("/api/firewall/connections") +def api_fw_connections(): + return api_wrap(lambda: ssh_run( + "echo '=== Established Connections ===' && " + "ss -tnp state established 2>&1 | head -50" + )) + + +@app.route("/api/firewall/connection-stats") +def api_fw_conn_stats(): + return api_wrap(lambda: ssh_run( + "echo '=== Connection States ===' && " + "ss -s 2>&1 && " + "echo '' && echo '=== By State ===' && " + "ss -tan 2>/dev/null | awk 'NR>1{print $1}' | sort | uniq -c | sort -rn" + )) + + +@app.route("/api/firewall/top-ips") +def api_fw_top_ips(): + return api_wrap(lambda: ssh_run( + "echo '=== Top 20 IPs by Connection Count ===' && " + "ss -tn state established 2>/dev/null | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head -20" + )) + + +@app.route("/api/firewall/blocked") +def api_fw_blocked(): + return api_wrap(lambda: ssh_run( + "echo '=== Recent Blocked/Dropped ===' && " + "(grep -i 'block\\|drop\\|reject\\|denied\\|UFW BLOCK' /var/log/kern.log 2>/dev/null || " + "grep -i 'block\\|drop\\|reject\\|denied\\|UFW BLOCK' /var/log/syslog 2>/dev/null || " + "journalctl -k --no-pager -n 50 2>/dev/null | grep -i 'block\\|drop\\|reject') | tail -30" + )) + + +@app.route("/api/firewall/log") +def api_fw_log(): + return api_wrap(lambda: ssh_run( + "echo '=== Firewall Log ===' && " + "(tail -50 /var/log/ufw.log 2>/dev/null || " + "grep -i 'UFW\\|iptables\\|nft\\|firewalld\\|csf\\|lfd' /var/log/syslog 2>/dev/null | tail -50 || " + "journalctl -k --no-pager -n 50 2>/dev/null)" + )) + + +# ── API: Firewall - UFW extras ────────────────────────────────────── +@app.route("/api/firewall/ufw/numbered") +def api_fw_ufw_numbered(): + return api_wrap(lambda: ssh_run("ufw status numbered 2>&1")) + + +@app.route("/api/firewall/ufw/default", methods=["POST"]) +def api_fw_ufw_default(): + data = request.json or {} + policy = data.get("policy", "deny") + direction = data.get("direction", "incoming") + return api_wrap(lambda: ssh_run(f"ufw default {policy} {direction} 2>&1 && ufw status verbose 2>&1")) + + +@app.route("/api/firewall/ufw/app-list") +def api_fw_ufw_app_list(): + return api_wrap(lambda: ssh_run("ufw app list 2>&1")) + + +@app.route("/api/firewall/ufw/log") +def api_fw_ufw_log(): + return api_wrap(lambda: ssh_run("tail -50 /var/log/ufw.log 2>/dev/null || echo 'No UFW log found'")) + + +@app.route("/api/firewall/ufw/log-level", methods=["POST"]) +def api_fw_ufw_log_level(): + level = (request.json or {}).get("level", "on") + return api_wrap(lambda: ssh_run(f"ufw logging {level} 2>&1")) + + +# ── API: Firewall - iptables ──────────────────────────────────────── +@app.route("/api/firewall/iptables/list") +def api_fw_ipt_list(): + return api_wrap(lambda: ssh_run(iptables.list_cmd())) + + +@app.route("/api/firewall/iptables/list-nat") +def api_fw_ipt_list_nat(): + return api_wrap(lambda: ssh_run(iptables.list_nat_cmd())) + + +@app.route("/api/firewall/iptables/list-mangle") +def api_fw_ipt_list_mangle(): + return api_wrap(lambda: ssh_run(iptables.list_mangle_cmd())) + + +@app.route("/api/firewall/iptables/counters") +def api_fw_ipt_counters(): + return api_wrap(lambda: ssh_run(iptables.counters_cmd())) + + +@app.route("/api/firewall/iptables/ip6") +def api_fw_ipt_ip6(): + return api_wrap(lambda: ssh_run(iptables.ip6_list_cmd())) + + +@app.route("/api/firewall/iptables/add", methods=["POST"]) +def api_fw_ipt_add(): + data = request.json or {} + chain = data.get("chain", "INPUT") + rule = data.get("rule", "") + if not rule: + return jsonify({"ok": False, "error": "need rule"}), 400 + return api_wrap(lambda: ssh_run(iptables.add_rule_cmd(chain, rule))) + + +@app.route("/api/firewall/iptables/insert", methods=["POST"]) +def api_fw_ipt_insert(): + data = request.json or {} + chain = data.get("chain", "INPUT") + position = data.get("position", 1) + rule = data.get("rule", "") + if not rule: + return jsonify({"ok": False, "error": "need rule"}), 400 + return api_wrap(lambda: ssh_run(iptables.insert_rule_cmd(chain, position, rule))) + + +@app.route("/api/firewall/iptables/delete", methods=["POST"]) +def api_fw_ipt_delete(): + data = request.json or {} + chain = data.get("chain", "INPUT") + rule_num = data.get("rule_num", 0) + if not rule_num: + return jsonify({"ok": False, "error": "need rule_num"}), 400 + return api_wrap(lambda: ssh_run(iptables.delete_rule_cmd(chain, rule_num))) + + +@app.route("/api/firewall/iptables/policy", methods=["POST"]) +def api_fw_ipt_policy(): + data = request.json or {} + chain = data.get("chain", "INPUT") + target = data.get("target", "ACCEPT") + return api_wrap(lambda: ssh_run(iptables.policy_cmd(chain, target))) + + +@app.route("/api/firewall/iptables/block-ip", methods=["POST"]) +def api_fw_ipt_block(): + ip = (request.json or {}).get("ip", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(iptables.block_ip_cmd(ip))) + + +@app.route("/api/firewall/iptables/unblock-ip", methods=["POST"]) +def api_fw_ipt_unblock(): + ip = (request.json or {}).get("ip", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(iptables.unblock_ip_cmd(ip))) + + +@app.route("/api/firewall/iptables/blocked") +def api_fw_ipt_blocked(): + return api_wrap(lambda: ssh_run(iptables.list_blocked_cmd())) + + +@app.route("/api/firewall/iptables/save", methods=["POST"]) +def api_fw_ipt_save(): + return api_wrap(lambda: ssh_run(iptables.save_cmd())) + + +@app.route("/api/firewall/iptables/restore", methods=["POST"]) +def api_fw_ipt_restore(): + return api_wrap(lambda: ssh_run(iptables.restore_cmd())) + + +@app.route("/api/firewall/iptables/flush", methods=["POST"]) +def api_fw_ipt_flush(): + return api_wrap(lambda: ssh_run(iptables.flush_cmd())) + + +@app.route("/api/firewall/iptables/zero", methods=["POST"]) +def api_fw_ipt_zero(): + return api_wrap(lambda: ssh_run(iptables.zero_counters_cmd())) + + +@app.route("/api/firewall/iptables/log") +def api_fw_ipt_log(): + return api_wrap(lambda: ssh_run(iptables.log_cmd())) + + +@app.route("/api/firewall/iptables/install", methods=["POST"]) +def api_fw_ipt_install(): + return api_wrap(lambda: ssh_run( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y iptables iptables-persistent 2>&1 && " + "echo 'iptables installed'", + timeout=60 + )) + + +# ── API: Firewall - nftables ──────────────────────────────────────── +@app.route("/api/firewall/nftables/list") +def api_fw_nft_list(): + return api_wrap(lambda: ssh_run(nftables.list_cmd())) + + +@app.route("/api/firewall/nftables/tables") +def api_fw_nft_tables(): + return api_wrap(lambda: ssh_run(nftables.list_tables_cmd())) + + +@app.route("/api/firewall/nftables/counters") +def api_fw_nft_counters(): + return api_wrap(lambda: ssh_run(nftables.counters_cmd())) + + +@app.route("/api/firewall/nftables/chains", methods=["POST"]) +def api_fw_nft_chains(): + table = (request.json or {}).get("table", "inet filter") + return api_wrap(lambda: ssh_run(nftables.list_chains_cmd(table))) + + +@app.route("/api/firewall/nftables/add-rule", methods=["POST"]) +def api_fw_nft_add_rule(): + data = request.json or {} + table = data.get("table", "inet filter") + chain = data.get("chain", "input") + rule = data.get("rule", "") + if not rule: + return jsonify({"ok": False, "error": "need rule"}), 400 + return api_wrap(lambda: ssh_run(nftables.add_rule_cmd(table, chain, rule))) + + +@app.route("/api/firewall/nftables/delete-rule", methods=["POST"]) +def api_fw_nft_del_rule(): + data = request.json or {} + table = data.get("table", "inet filter") + chain = data.get("chain", "input") + handle = data.get("handle", 0) + if not handle: + return jsonify({"ok": False, "error": "need handle"}), 400 + return api_wrap(lambda: ssh_run(nftables.delete_rule_cmd(table, chain, handle))) + + +@app.route("/api/firewall/nftables/create-table", methods=["POST"]) +def api_fw_nft_create_table(): + data = request.json or {} + family = data.get("family", "inet") + name = data.get("name", "") + if not name: + return jsonify({"ok": False, "error": "need name"}), 400 + return api_wrap(lambda: ssh_run(nftables.create_table_cmd(family, name))) + + +@app.route("/api/firewall/nftables/create-chain", methods=["POST"]) +def api_fw_nft_create_chain(): + data = request.json or {} + table = data.get("table", "inet filter") + chain = data.get("chain", "") + hook = data.get("hook", "input") + if not chain: + return jsonify({"ok": False, "error": "need chain name"}), 400 + return api_wrap(lambda: ssh_run(nftables.create_chain_cmd(table, chain, "filter", hook))) + + +@app.route("/api/firewall/nftables/save", methods=["POST"]) +def api_fw_nft_save(): + return api_wrap(lambda: ssh_run(nftables.save_cmd())) + + +@app.route("/api/firewall/nftables/restore", methods=["POST"]) +def api_fw_nft_restore(): + return api_wrap(lambda: ssh_run(nftables.restore_cmd())) + + +@app.route("/api/firewall/nftables/config") +def api_fw_nft_config(): + return api_wrap(lambda: ssh_run(nftables.config_cmd())) + + +@app.route("/api/firewall/nftables/flush", methods=["POST"]) +def api_fw_nft_flush(): + return api_wrap(lambda: ssh_run(nftables.flush_cmd())) + + +@app.route("/api/firewall/nftables/install", methods=["POST"]) +def api_fw_nft_install(): + return api_wrap(lambda: ssh_run(nftables.install_cmd(), timeout=60)) + + +# ── API: Firewall - firewalld ─────────────────────────────────────── +@app.route("/api/firewall/firewalld/status") +def api_fw_fwd_status(): + return api_wrap(lambda: ssh_run(firewalld.status_cmd())) + + +@app.route("/api/firewall/firewalld/zones") +def api_fw_fwd_zones(): + return api_wrap(lambda: ssh_run(firewalld.zones_cmd())) + + +@app.route("/api/firewall/firewalld/zone-info", methods=["POST"]) +def api_fw_fwd_zone_info(): + zone = (request.json or {}).get("zone", "public") + return api_wrap(lambda: ssh_run(firewalld.zone_info_cmd(zone))) + + +@app.route("/api/firewall/firewalld/default-zone", methods=["POST"]) +def api_fw_fwd_default_zone(): + zone = (request.json or {}).get("zone", "public") + return api_wrap(lambda: ssh_run(firewalld.default_zone_cmd(zone))) + + +@app.route("/api/firewall/firewalld/add-service", methods=["POST"]) +def api_fw_fwd_add_service(): + data = request.json or {} + service = data.get("service", "") + zone = data.get("zone", "public") + if not service: + return jsonify({"ok": False, "error": "need service"}), 400 + return api_wrap(lambda: ssh_run(firewalld.add_service_cmd(service, zone))) + + +@app.route("/api/firewall/firewalld/remove-service", methods=["POST"]) +def api_fw_fwd_remove_service(): + data = request.json or {} + service = data.get("service", "") + zone = data.get("zone", "public") + if not service: + return jsonify({"ok": False, "error": "need service"}), 400 + return api_wrap(lambda: ssh_run(firewalld.remove_service_cmd(service, zone))) + + +@app.route("/api/firewall/firewalld/add-port", methods=["POST"]) +def api_fw_fwd_add_port(): + data = request.json or {} + port = data.get("port", "") + zone = data.get("zone", "public") + if not port: + return jsonify({"ok": False, "error": "need port"}), 400 + return api_wrap(lambda: ssh_run(firewalld.add_port_cmd(port, zone))) + + +@app.route("/api/firewall/firewalld/remove-port", methods=["POST"]) +def api_fw_fwd_remove_port(): + data = request.json or {} + port = data.get("port", "") + zone = data.get("zone", "public") + if not port: + return jsonify({"ok": False, "error": "need port"}), 400 + return api_wrap(lambda: ssh_run(firewalld.remove_port_cmd(port, zone))) + + +@app.route("/api/firewall/firewalld/add-rich-rule", methods=["POST"]) +def api_fw_fwd_add_rich(): + data = request.json or {} + rule = data.get("rule", "") + zone = data.get("zone", "public") + if not rule: + return jsonify({"ok": False, "error": "need rule"}), 400 + return api_wrap(lambda: ssh_run(firewalld.add_rich_rule_cmd(rule, zone))) + + +@app.route("/api/firewall/firewalld/remove-rich-rule", methods=["POST"]) +def api_fw_fwd_remove_rich(): + data = request.json or {} + rule = data.get("rule", "") + zone = data.get("zone", "public") + if not rule: + return jsonify({"ok": False, "error": "need rule"}), 400 + return api_wrap(lambda: ssh_run(firewalld.remove_rich_rule_cmd(rule, zone))) + + +@app.route("/api/firewall/firewalld/block-ip", methods=["POST"]) +def api_fw_fwd_block(): + ip = (request.json or {}).get("ip", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(firewalld.block_ip_cmd(ip))) + + +@app.route("/api/firewall/firewalld/unblock-ip", methods=["POST"]) +def api_fw_fwd_unblock(): + ip = (request.json or {}).get("ip", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(firewalld.unblock_ip_cmd(ip))) + + +@app.route("/api/firewall/firewalld/reload", methods=["POST"]) +def api_fw_fwd_reload(): + return api_wrap(lambda: ssh_run(firewalld.reload_cmd())) + + +@app.route("/api/firewall/firewalld/panic-on", methods=["POST"]) +def api_fw_fwd_panic_on(): + return api_wrap(lambda: ssh_run(firewalld.panic_on_cmd())) + + +@app.route("/api/firewall/firewalld/panic-off", methods=["POST"]) +def api_fw_fwd_panic_off(): + return api_wrap(lambda: ssh_run(firewalld.panic_off_cmd())) + + +@app.route("/api/firewall/firewalld/services-list") +def api_fw_fwd_services_list(): + return api_wrap(lambda: ssh_run(firewalld.services_list_cmd())) + + +@app.route("/api/firewall/firewalld/log") +def api_fw_fwd_log(): + return api_wrap(lambda: ssh_run(firewalld.log_cmd())) + + +@app.route("/api/firewall/firewalld/install", methods=["POST"]) +def api_fw_fwd_install(): + return api_wrap(lambda: ssh_run(firewalld.install_cmd(), timeout=60)) + + +# ── API: Firewall - CSF ───────────────────────────────────────────── +@app.route("/api/firewall/csf/status") +def api_fw_csf_status(): + return api_wrap(lambda: ssh_run(csf.status_cmd())) + + +@app.route("/api/firewall/csf/start", methods=["POST"]) +def api_fw_csf_start(): + return api_wrap(lambda: ssh_run(csf.start_cmd())) + + +@app.route("/api/firewall/csf/stop", methods=["POST"]) +def api_fw_csf_stop(): + return api_wrap(lambda: ssh_run(csf.stop_cmd())) + + +@app.route("/api/firewall/csf/restart", methods=["POST"]) +def api_fw_csf_restart(): + return api_wrap(lambda: ssh_run(csf.restart_cmd())) + + +@app.route("/api/firewall/csf/list") +def api_fw_csf_list(): + return api_wrap(lambda: ssh_run(csf.list_cmd())) + + +@app.route("/api/firewall/csf/allow", methods=["POST"]) +def api_fw_csf_allow(): + data = request.json or {} + ip = data.get("ip", "") + comment = data.get("comment", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(csf.allow_ip_cmd(ip, comment))) + + +@app.route("/api/firewall/csf/deny", methods=["POST"]) +def api_fw_csf_deny(): + data = request.json or {} + ip = data.get("ip", "") + comment = data.get("comment", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(csf.deny_ip_cmd(ip, comment))) + + +@app.route("/api/firewall/csf/remove", methods=["POST"]) +def api_fw_csf_remove(): + ip = (request.json or {}).get("ip", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(csf.remove_ip_cmd(ip))) + + +@app.route("/api/firewall/csf/grep", methods=["POST"]) +def api_fw_csf_grep(): + ip = (request.json or {}).get("ip", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(csf.grep_ip_cmd(ip))) + + +@app.route("/api/firewall/csf/temp-allow", methods=["POST"]) +def api_fw_csf_temp_allow(): + data = request.json or {} + ip = data.get("ip", "") + ttl = data.get("ttl", 3600) + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(csf.temp_allow_cmd(ip, ttl))) + + +@app.route("/api/firewall/csf/temp-deny", methods=["POST"]) +def api_fw_csf_temp_deny(): + data = request.json or {} + ip = data.get("ip", "") + ttl = data.get("ttl", 3600) + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(csf.temp_deny_cmd(ip, ttl))) + + +@app.route("/api/firewall/csf/temp-list") +def api_fw_csf_temp_list(): + return api_wrap(lambda: ssh_run(csf.temp_list_cmd())) + + +@app.route("/api/firewall/csf/config") +def api_fw_csf_config(): + return api_wrap(lambda: ssh_run(csf.config_cmd())) + + +@app.route("/api/firewall/csf/log") +def api_fw_csf_log(): + return api_wrap(lambda: ssh_run(csf.log_cmd())) + + +@app.route("/api/firewall/csf/test") +def api_fw_csf_test(): + return api_wrap(lambda: ssh_run(csf.test_cmd())) + + +@app.route("/api/firewall/csf/install", methods=["POST"]) +def api_fw_csf_install(): + return api_wrap(lambda: ssh_run(csf.install_cmd(), timeout=120)) + + +# ── API: Firewall - Migration (UFW↔iptables) ──────────────────────── +@app.route("/api/firewall/migrate/ufw-to-iptables", methods=["POST"]) +def api_fw_migrate_ufw_to_ipt(): + return api_wrap(lambda: ssh_run( + "echo '=== Migrating UFW → iptables ===' && " + "echo '1. Exporting current UFW rules...' && " + "ufw status numbered 2>&1 && " + "echo '' && echo '2. Saving iptables state (UFW generates iptables rules)...' && " + "mkdir -p /etc/iptables && " + "iptables-save > /etc/iptables/rules.v4.pre-migration 2>&1 && " + "ip6tables-save > /etc/iptables/rules.v6.pre-migration 2>&1 && " + "echo '3. Disabling UFW...' && " + "echo 'y' | ufw disable 2>&1 && " + "systemctl disable ufw 2>&1 && " + "echo '4. Restoring iptables rules from UFW state...' && " + "iptables-restore < /etc/iptables/rules.v4.pre-migration 2>&1 && " + "echo '5. Installing iptables-persistent...' && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent 2>&1 && " + "iptables-save > /etc/iptables/rules.v4 2>&1 && " + "ip6tables-save > /etc/iptables/rules.v6 2>&1 && " + "systemctl enable netfilter-persistent 2>&1 && " + "echo '' && echo '=== Migration Complete ===' && " + "echo 'UFW disabled. iptables rules preserved and persisted.' && " + "echo 'Current iptables rules:' && iptables -L -n --line-numbers 2>&1 | head -30", + timeout=60 + )) + + +@app.route("/api/firewall/migrate/iptables-to-ufw", methods=["POST"]) +def api_fw_migrate_ipt_to_ufw(): + return api_wrap(lambda: ssh_run( + "echo '=== Migrating iptables → UFW ===' && " + "echo '1. Saving current iptables rules as backup...' && " + "mkdir -p /etc/iptables && " + "iptables-save > /etc/iptables/rules.v4.pre-ufw-migration 2>&1 && " + "echo '2. Current iptables ACCEPT rules (will be converted):' && " + "iptables -L INPUT -n --line-numbers 2>/dev/null | grep ACCEPT | " + "awk '{if($5==\"tcp\"||$5==\"udp\") print $5\" \"$NF}' | sed 's/dpt://' && " + "echo '' && echo '3. Installing and resetting UFW...' && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y ufw 2>&1 && " + "echo 'y' | ufw reset 2>&1 && " + "ufw default deny incoming 2>&1 && " + "ufw default allow outgoing 2>&1 && " + "echo '4. Converting iptables rules to UFW...' && " + "iptables -L INPUT -n 2>/dev/null | grep ACCEPT | " + "awk '{if($5==\"tcp\") print \"allow \"$NF\"/tcp\"; if($5==\"udp\") print \"allow \"$NF\"/udp\"}' | " + "sed 's/dpt://' | while read rule; do echo \"+ ufw $rule\" && ufw $rule 2>&1; done && " + "echo '5. Enabling UFW...' && " + "echo 'y' | ufw enable 2>&1 && " + "echo '6. Disabling iptables-persistent...' && " + "systemctl disable netfilter-persistent 2>/dev/null; " + "echo '' && echo '=== Migration Complete ===' && " + "ufw status verbose 2>&1", + timeout=60 + )) + + +# ── API: Security - Hardening ──────────────────────────────────────── +@app.route("/api/security/ssh/status") +def api_sec_ssh_status(): + return api_wrap(lambda: ssh_run(hardening.ssh_status_cmd())) + + +@app.route("/api/security/ssh/harden", methods=["POST"]) +def api_sec_ssh_harden(): + data = request.json or {} + port = data.get("port", 22) + disable_root = data.get("disable_root", True) + disable_password = data.get("disable_password", True) + return api_wrap(lambda: ssh_run(hardening.ssh_harden_cmd(port, disable_root, disable_password))) + + +@app.route("/api/security/kernel/status") +def api_sec_kernel_status(): + return api_wrap(lambda: ssh_run(hardening.kernel_status_cmd())) + + +@app.route("/api/security/kernel/harden", methods=["POST"]) +def api_sec_kernel_harden(): + return api_wrap(lambda: ssh_run(hardening.kernel_harden_cmd())) + + +@app.route("/api/security/auto-updates", methods=["POST"]) +def api_sec_auto_updates(): + return api_wrap(lambda: ssh_run(hardening.auto_updates_cmd(), timeout=60)) + + +@app.route("/api/security/user-audit") +def api_sec_user_audit(): + return api_wrap(lambda: ssh_run(hardening.user_audit_cmd(), timeout=60)) + + +@app.route("/api/security/port-scan") +def api_sec_port_scan(): + return api_wrap(lambda: ssh_run(hardening.port_scan_cmd())) + + +# ── API: Security - Firewall ──────────────────────────────────────── +@app.route("/api/security/firewall/status") +def api_sec_fw_status(): + return api_wrap(lambda: ssh_run(hardening.firewall_status_cmd())) + + +@app.route("/api/security/firewall/enable", methods=["POST"]) +def api_sec_fw_enable(): + port = (request.json or {}).get("ssh_port", 22) + return api_wrap(lambda: ssh_run(hardening.firewall_enable_cmd(port), timeout=60)) + + +@app.route("/api/security/firewall/disable", methods=["POST"]) +def api_sec_fw_disable(): + return api_wrap(lambda: ssh_run("echo 'y' | ufw disable 2>&1 && ufw status verbose 2>&1")) + + +@app.route("/api/security/firewall/add", methods=["POST"]) +def api_sec_fw_add(): + rule = request.json.get("rule", "") + if not rule: + return jsonify({"ok": False, "error": "need rule"}), 400 + return api_wrap(lambda: ssh_run(hardening.firewall_add_rule_cmd(rule))) + + +@app.route("/api/security/firewall/delete", methods=["POST"]) +def api_sec_fw_delete(): + rule = request.json.get("rule", "") + if not rule: + return jsonify({"ok": False, "error": "need rule"}), 400 + return api_wrap(lambda: ssh_run(hardening.firewall_delete_rule_cmd(rule))) + + +@app.route("/api/security/firewall/preset", methods=["POST"]) +def api_sec_fw_preset(): + preset = request.json.get("preset", "") + if not preset: + return jsonify({"ok": False, "error": "need preset name"}), 400 + return api_wrap(lambda: ssh_run(hardening.firewall_preset_cmd(preset))) + + +# ── API: Security - Security Apps ─────────────────────────────────── +@app.route("/api/security/apps") +def api_sec_apps(): + return jsonify({"ok": True, "data": security_apps.SECURITY_APPS, + "categories": security_apps.CATEGORIES}) + + +@app.route("/api/security/apps/check", methods=["POST"]) +def api_sec_apps_check(): + name = request.json.get("name", "") + app_entry = next((a for a in security_apps.SECURITY_APPS if a["name"] == name), None) + if not app_entry: + return jsonify({"ok": False, "error": f"App '{name}' not found"}), 400 + return api_wrap(lambda: ssh_run(app_entry["check"])) + + +@app.route("/api/security/apps/install", methods=["POST"]) +def api_sec_apps_install(): + name = request.json.get("name", "") + app_entry = next((a for a in security_apps.SECURITY_APPS if a["name"] == name), None) + if not app_entry: + return jsonify({"ok": False, "error": f"App '{name}' not found"}), 400 + return api_wrap(lambda: ssh_run(app_entry["install"], timeout=120)) + + +@app.route("/api/security/apps/scan", methods=["POST"]) +def api_sec_apps_scan(): + name = request.json.get("name", "") + app_entry = next((a for a in security_apps.SECURITY_APPS if a["name"] == name), None) + if not app_entry: + return jsonify({"ok": False, "error": f"App '{name}' not found"}), 400 + return api_wrap(lambda: ssh_run(app_entry["scan"], timeout=120)) + + +@app.route("/api/security/apps/uninstall", methods=["POST"]) +def api_sec_apps_uninstall(): + name = request.json.get("name", "") + app_entry = next((a for a in security_apps.SECURITY_APPS if a["name"] == name), None) + if not app_entry: + return jsonify({"ok": False, "error": f"App '{name}' not found"}), 400 + return api_wrap(lambda: ssh_run(app_entry["uninstall"], timeout=60)) + + +# ── API: Security - SSL/TLS ───────────────────────────────────────── +@app.route("/api/security/ssl/check", methods=["POST"]) +def api_sec_ssl_check(): + domain = request.json.get("domain", "") + if not domain: + return jsonify({"ok": False, "error": "need domain"}), 400 + return api_wrap(lambda: ssh_run(ssl_audit.ssl_check_cmd(domain), timeout=30)) + + +@app.route("/api/security/ssl/expiry", methods=["POST"]) +def api_sec_ssl_expiry(): + domain = request.json.get("domain", "") + if not domain: + return jsonify({"ok": False, "error": "need domain"}), 400 + return api_wrap(lambda: ssh_run(ssl_audit.ssl_expiry_cmd(domain))) + + +@app.route("/api/security/ssl/expiry-all") +def api_sec_ssl_expiry_all(): + return api_wrap(lambda: ssh_run(ssl_audit.ssl_expiry_all_cmd())) + + +@app.route("/api/security/ssl/grade", methods=["POST"]) +def api_sec_ssl_grade(): + domain = request.json.get("domain", "") + if not domain: + return jsonify({"ok": False, "error": "need domain"}), 400 + return api_wrap(lambda: ssh_run(ssl_audit.ssl_grade_cmd(domain), timeout=60)) + + +@app.route("/api/security/ssl/renew", methods=["POST"]) +def api_sec_ssl_renew(): + return api_wrap(lambda: ssh_run(ssl_audit.ssl_renew_cmd(), timeout=120)) + + +@app.route("/api/security/ssl/renew-dry") +def api_sec_ssl_renew_dry(): + return api_wrap(lambda: ssh_run(ssl_audit.ssl_renew_dry_cmd(), timeout=60)) + + +@app.route("/api/security/ssl/autorenew-status") +def api_sec_ssl_autorenew(): + return api_wrap(lambda: ssh_run(ssl_audit.ssl_autorenew_status_cmd())) + + +@app.route("/api/security/ssl/security-headers", methods=["POST"]) +def api_sec_ssl_headers(): + domain = request.json.get("domain", "") + if not domain: + return jsonify({"ok": False, "error": "need domain"}), 400 + return api_wrap(lambda: ssh_run(ssl_audit.security_headers_cmd(domain))) + + +# ── API: Security - Monitoring/IDS ────────────────────────────────── +@app.route("/api/security/monitoring/auth-log") +def api_sec_auth_log(): + lines = request.args.get("lines", 100, type=int) + return api_wrap(lambda: ssh_run(monitoring.auth_log_cmd(lines))) + + +@app.route("/api/security/monitoring/login-tracker") +def api_sec_login_tracker(): + return api_wrap(lambda: ssh_run(monitoring.login_tracker_cmd(), timeout=60)) + + +@app.route("/api/security/monitoring/active-sessions") +def api_sec_active_sessions(): + return api_wrap(lambda: ssh_run(monitoring.active_sessions_cmd())) + + +@app.route("/api/security/monitoring/file-integrity") +def api_sec_file_integrity(): + paths = request.args.get("paths", "/etc /usr/bin /usr/sbin /var/www") + return api_wrap(lambda: ssh_run(monitoring.file_integrity_check_cmd(paths), timeout=120)) + + +@app.route("/api/security/monitoring/file-integrity/init", methods=["POST"]) +def api_sec_file_integrity_init(): + paths = (request.json or {}).get("paths", "/etc /usr/bin /usr/sbin /var/www") + return api_wrap(lambda: ssh_run(monitoring.file_integrity_init_cmd(paths), timeout=120)) + + +@app.route("/api/security/monitoring/process-audit") +def api_sec_process_audit(): + return api_wrap(lambda: ssh_run(monitoring.process_audit_cmd(), timeout=60)) + + +@app.route("/api/security/monitoring/security-log") +def api_sec_security_log(): + lines = request.args.get("lines", 100, type=int) + return api_wrap(lambda: ssh_run(monitoring.security_log_cmd(lines), timeout=60)) + + +@app.route("/api/security/monitoring/suid-audit") +def api_sec_suid_audit(): + return api_wrap(lambda: ssh_run(monitoring.suid_audit_cmd(), timeout=60)) + + +@app.route("/api/security/monitoring/world-writable") +def api_sec_world_writable(): + return api_wrap(lambda: ssh_run(monitoring.world_writable_cmd(), timeout=60)) + + +@app.route("/api/security/monitoring/cron-audit") +def api_sec_cron_audit(): + return api_wrap(lambda: ssh_run(monitoring.cron_audit_cmd(), timeout=60)) + + +@app.route("/api/security/monitoring/alerts/setup", methods=["POST"]) +def api_sec_alert_setup(): + data = request.json or {} + email = data.get("email", "") + webhook = data.get("webhook", "") + if not email: + return jsonify({"ok": False, "error": "need email"}), 400 + return api_wrap(lambda: ssh_run(monitoring.alert_setup_cmd(email, webhook))) + + +@app.route("/api/security/monitoring/alerts/status") +def api_sec_alert_status(): + return api_wrap(lambda: ssh_run(monitoring.alert_status_cmd())) + + +@app.route("/api/security/monitoring/alerts/remove", methods=["POST"]) +def api_sec_alert_remove(): + return api_wrap(lambda: ssh_run(monitoring.alert_remove_cmd())) + + +# ── API: Security - DDoS Protection ───────────────────────────────── +@app.route("/api/security/ddos/connection-stats") +def api_sec_ddos_conn_stats(): + return api_wrap(lambda: ssh_run(ddos.connection_stats_cmd())) + + +@app.route("/api/security/ddos/syn-flood") +def api_sec_ddos_syn(): + return api_wrap(lambda: ssh_run(ddos.syn_flood_check_cmd())) + + +@app.route("/api/security/ddos/bandwidth") +def api_sec_ddos_bandwidth(): + return api_wrap(lambda: ssh_run(ddos.bandwidth_stats_cmd(), timeout=30)) + + +@app.route("/api/security/ddos/rate-limit/status") +def api_sec_ddos_rate_status(): + return api_wrap(lambda: ssh_run(ddos.nginx_rate_limit_status_cmd())) + + +@app.route("/api/security/ddos/rate-limit/enable", methods=["POST"]) +def api_sec_ddos_rate_enable(): + data = request.json or {} + rps = data.get("requests_per_second", 10) + burst = data.get("burst", 20) + return api_wrap(lambda: ssh_run(ddos.nginx_rate_limit_cmd(rps, burst))) + + +@app.route("/api/security/ddos/rate-limit/remove", methods=["POST"]) +def api_sec_ddos_rate_remove(): + return api_wrap(lambda: ssh_run(ddos.nginx_rate_limit_remove_cmd())) + + +@app.route("/api/security/ddos/auto-blacklist/enable", methods=["POST"]) +def api_sec_ddos_blacklist_enable(): + data = request.json or {} + threshold = data.get("threshold", 100) + return api_wrap(lambda: ssh_run(ddos.auto_blacklist_cmd(threshold))) + + +@app.route("/api/security/ddos/auto-blacklist/status") +def api_sec_ddos_blacklist_status(): + return api_wrap(lambda: ssh_run(ddos.auto_blacklist_status_cmd())) + + +@app.route("/api/security/ddos/auto-blacklist/remove", methods=["POST"]) +def api_sec_ddos_blacklist_remove(): + return api_wrap(lambda: ssh_run(ddos.auto_blacklist_remove_cmd())) + + +@app.route("/api/security/ddos/blacklist/add", methods=["POST"]) +def api_sec_ddos_blacklist_add(): + ip = request.json.get("ip", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(ddos.blacklist_ip_cmd(ip))) + + +@app.route("/api/security/ddos/blacklist/remove", methods=["POST"]) +def api_sec_ddos_unblacklist(): + ip = request.json.get("ip", "") + if not ip: + return jsonify({"ok": False, "error": "need ip"}), 400 + return api_wrap(lambda: ssh_run(ddos.unblacklist_ip_cmd(ip))) + + +@app.route("/api/security/ddos/blacklist/list") +def api_sec_ddos_blacklist_list(): + return api_wrap(lambda: ssh_run(ddos.show_blacklist_cmd())) + + +@app.route("/api/security/ddos/syn-protection", methods=["POST"]) +def api_sec_ddos_syn_protect(): + return api_wrap(lambda: ssh_run(ddos.syn_protection_cmd())) + + +@app.route("/api/security/ddos/cloudflare/status", methods=["POST"]) +def api_sec_ddos_cf_status(): + data = request.json or {} + zone = data.get("zone_id", "") + token = data.get("api_token", "") + if not zone or not token: + return jsonify({"ok": False, "error": "need zone_id and api_token"}), 400 + return api_wrap(lambda: ssh_run(ddos.cloudflare_status_cmd(zone, token))) + + +@app.route("/api/security/ddos/cloudflare/toggle", methods=["POST"]) +def api_sec_ddos_cf_toggle(): + data = request.json or {} + zone = data.get("zone_id", "") + token = data.get("api_token", "") + enable = data.get("enable", True) + if not zone or not token: + return jsonify({"ok": False, "error": "need zone_id and api_token"}), 400 + return api_wrap(lambda: ssh_run(ddos.cloudflare_under_attack_cmd(zone, token, enable))) + + +@app.route("/api/security/ddos/tor-block", methods=["POST"]) +def api_sec_ddos_tor_block(): + enable = (request.json or {}).get("enable", True) + return api_wrap(lambda: ssh_run(ddos.tor_exit_block_cmd(enable), timeout=60)) + + +@app.route("/api/security/ddos/geoblock", methods=["POST"]) +def api_sec_ddos_geoblock(): + codes = request.json.get("countries", "") + if not codes: + return jsonify({"ok": False, "error": "need country codes"}), 400 + return api_wrap(lambda: ssh_run(ddos.geoblock_cmd(codes), timeout=60)) + + +# ── API: Security - Backup ────────────────────────────────────────── +@app.route("/api/security/backup/now", methods=["POST"]) +def api_sec_backup_now(): + data = request.json or {} + paths = data.get("paths", "/etc /var/www /opt/seteclabs /root") + dest = data.get("dest", "/var/backups/setec") + encrypt_pass = data.get("encrypt_pass", "") + remote_host = data.get("remote_host", "") + remote_path = data.get("remote_path", "") + return api_wrap(lambda: ssh_run( + backup.backup_now_cmd(paths, dest, encrypt_pass, remote_host, remote_path), + timeout=120 + )) + + +@app.route("/api/security/backup/list") +def api_sec_backup_list(): + dest = request.args.get("dest", "/var/backups/setec") + return api_wrap(lambda: ssh_run(backup.backup_list_cmd(dest))) + + +@app.route("/api/security/backup/restore", methods=["POST"]) +def api_sec_backup_restore(): + data = request.json or {} + archive = data.get("archive", "") + encrypt_pass = data.get("encrypt_pass", "") + if not archive: + return jsonify({"ok": False, "error": "need archive path"}), 400 + return api_wrap(lambda: ssh_run(backup.backup_restore_cmd(archive, "/", encrypt_pass), timeout=120)) + + +@app.route("/api/security/backup/delete", methods=["POST"]) +def api_sec_backup_delete(): + archive = request.json.get("archive", "") + if not archive: + return jsonify({"ok": False, "error": "need archive path"}), 400 + return api_wrap(lambda: ssh_run(backup.backup_delete_cmd(archive))) + + +@app.route("/api/security/backup/schedule", methods=["POST"]) +def api_sec_backup_schedule(): + data = request.json or {} + paths = data.get("paths", "/etc /var/www /opt/seteclabs /root") + dest = data.get("dest", "/var/backups/setec") + encrypt_pass = data.get("encrypt_pass", "") + schedule = data.get("schedule", "daily") + keep = data.get("keep", 7) + return api_wrap(lambda: ssh_run( + backup.backup_schedule_cmd(paths, dest, encrypt_pass, schedule, keep) + )) + + +@app.route("/api/security/backup/schedule/status") +def api_sec_backup_schedule_status(): + return api_wrap(lambda: ssh_run(backup.backup_schedule_status_cmd())) + + +@app.route("/api/security/backup/schedule/remove", methods=["POST"]) +def api_sec_backup_schedule_remove(): + return api_wrap(lambda: ssh_run(backup.backup_schedule_remove_cmd())) + + +# ── API: Security - ClamAV ─────────────────────────────────────────── +@app.route("/api/security/clamav/status") +def api_sec_clamav_status(): + return api_wrap(lambda: ssh_run(clamav.status_cmd())) + + +@app.route("/api/security/clamav/install", methods=["POST"]) +def api_sec_clamav_install(): + return api_wrap(lambda: ssh_run(clamav.install_cmd(), timeout=120)) + + +@app.route("/api/security/clamav/update-defs", methods=["POST"]) +def api_sec_clamav_update_defs(): + return api_wrap(lambda: ssh_run(clamav.update_defs_cmd(), timeout=120)) + + +@app.route("/api/security/clamav/scan", methods=["POST"]) +def api_sec_clamav_scan(): + data = request.json or {} + path = data.get("path", "/var/www") + recursive = data.get("recursive", True) + return api_wrap(lambda: ssh_run(clamav.scan_cmd(path, recursive), timeout=120)) + + +@app.route("/api/security/clamav/scan-quick", methods=["POST"]) +def api_sec_clamav_scan_quick(): + return api_wrap(lambda: ssh_run(clamav.scan_quick_cmd(), timeout=120)) + + +@app.route("/api/security/clamav/scan-full", methods=["POST"]) +def api_sec_clamav_scan_full(): + return api_wrap(lambda: ssh_run(clamav.scan_full_cmd(), timeout=120)) + + +@app.route("/api/security/clamav/quarantine-scan", methods=["POST"]) +def api_sec_clamav_quarantine_scan(): + data = request.json or {} + path = data.get("path", "/var/www") + return api_wrap(lambda: ssh_run(clamav.quarantine_scan_cmd(path), timeout=120)) + + +@app.route("/api/security/clamav/quarantine/list") +def api_sec_clamav_quarantine_list(): + return api_wrap(lambda: ssh_run(clamav.quarantine_list_cmd())) + + +@app.route("/api/security/clamav/quarantine/delete", methods=["POST"]) +def api_sec_clamav_quarantine_delete(): + return api_wrap(lambda: ssh_run(clamav.quarantine_delete_cmd())) + + +@app.route("/api/security/clamav/log") +def api_sec_clamav_log(): + lines = request.args.get("lines", 50, type=int) + return api_wrap(lambda: ssh_run(clamav.log_cmd(lines))) + + +@app.route("/api/security/clamav/schedule", methods=["POST"]) +def api_sec_clamav_schedule(): + data = request.json or {} + schedule = data.get("schedule", "daily") + paths = data.get("paths", "/") + return api_wrap(lambda: ssh_run(clamav.schedule_cmd(schedule, paths))) + + +@app.route("/api/security/clamav/schedule/status") +def api_sec_clamav_schedule_status(): + return api_wrap(lambda: ssh_run(clamav.schedule_status_cmd())) + + +@app.route("/api/security/clamav/schedule/remove", methods=["POST"]) +def api_sec_clamav_schedule_remove(): + return api_wrap(lambda: ssh_run(clamav.schedule_remove_cmd())) + + +@app.route("/api/security/clamav/config") +def api_sec_clamav_config(): + return api_wrap(lambda: ssh_run(clamav.config_cmd())) + + +@app.route("/api/security/clamav/uninstall", methods=["POST"]) +def api_sec_clamav_uninstall(): + return api_wrap(lambda: ssh_run(clamav.uninstall_cmd(), timeout=60)) + + +# ── API: Security - rkhunter ───────────────────────────────────────── +@app.route("/api/security/rkhunter/status") +def api_sec_rkh_status(): + return api_wrap(lambda: ssh_run(rkhunter.status_cmd())) + + +@app.route("/api/security/rkhunter/install", methods=["POST"]) +def api_sec_rkh_install(): + return api_wrap(lambda: ssh_run(rkhunter.install_cmd(), timeout=120)) + + +@app.route("/api/security/rkhunter/update", methods=["POST"]) +def api_sec_rkh_update(): + return api_wrap(lambda: ssh_run(rkhunter.update_cmd(), timeout=60)) + + +@app.route("/api/security/rkhunter/check", methods=["POST"]) +def api_sec_rkh_check(): + return api_wrap(lambda: ssh_run(rkhunter.check_cmd(), timeout=120)) + + +@app.route("/api/security/rkhunter/check-quick", methods=["POST"]) +def api_sec_rkh_check_quick(): + return api_wrap(lambda: ssh_run(rkhunter.check_quick_cmd(), timeout=60)) + + +@app.route("/api/security/rkhunter/log") +def api_sec_rkh_log(): + return api_wrap(lambda: ssh_run(rkhunter.log_cmd())) + + +@app.route("/api/security/rkhunter/config") +def api_sec_rkh_config(): + return api_wrap(lambda: ssh_run(rkhunter.config_cmd())) + + +@app.route("/api/security/rkhunter/whitelist") +def api_sec_rkh_whitelist(): + return api_wrap(lambda: ssh_run(rkhunter.whitelist_cmd())) + + +@app.route("/api/security/rkhunter/whitelist/add", methods=["POST"]) +def api_sec_rkh_whitelist_add(): + item = (request.json or {}).get("item", "") + if not item: + return jsonify({"ok": False, "error": "need item"}), 400 + return api_wrap(lambda: ssh_run(rkhunter.whitelist_add_cmd(item))) + + +@app.route("/api/security/rkhunter/schedule", methods=["POST"]) +def api_sec_rkh_schedule(): + schedule = (request.json or {}).get("schedule", "daily") + return api_wrap(lambda: ssh_run(rkhunter.schedule_cmd(schedule))) + + +@app.route("/api/security/rkhunter/schedule/status") +def api_sec_rkh_schedule_status(): + return api_wrap(lambda: ssh_run(rkhunter.schedule_status_cmd())) + + +@app.route("/api/security/rkhunter/schedule/remove", methods=["POST"]) +def api_sec_rkh_schedule_remove(): + return api_wrap(lambda: ssh_run(rkhunter.schedule_remove_cmd())) + + +@app.route("/api/security/rkhunter/uninstall", methods=["POST"]) +def api_sec_rkh_uninstall(): + return api_wrap(lambda: ssh_run(rkhunter.uninstall_cmd(), timeout=60)) + + +# ── API: Security - chkrootkit ─────────────────────────────────────── +@app.route("/api/security/chkrootkit/status") +def api_sec_chk_status(): + return api_wrap(lambda: ssh_run(chkrootkit.status_cmd())) + + +@app.route("/api/security/chkrootkit/install", methods=["POST"]) +def api_sec_chk_install(): + return api_wrap(lambda: ssh_run(chkrootkit.install_cmd(), timeout=120)) + + +@app.route("/api/security/chkrootkit/check", methods=["POST"]) +def api_sec_chk_check(): + return api_wrap(lambda: ssh_run(chkrootkit.check_cmd(), timeout=120)) + + +@app.route("/api/security/chkrootkit/check-expert", methods=["POST"]) +def api_sec_chk_expert(): + return api_wrap(lambda: ssh_run(chkrootkit.check_expert_cmd(), timeout=120)) + + +@app.route("/api/security/chkrootkit/log") +def api_sec_chk_log(): + return api_wrap(lambda: ssh_run(chkrootkit.log_cmd())) + + +@app.route("/api/security/chkrootkit/config") +def api_sec_chk_config(): + return api_wrap(lambda: ssh_run(chkrootkit.config_cmd())) + + +@app.route("/api/security/chkrootkit/schedule", methods=["POST"]) +def api_sec_chk_schedule(): + schedule = (request.json or {}).get("schedule", "daily") + return api_wrap(lambda: ssh_run(chkrootkit.schedule_cmd(schedule))) + + +@app.route("/api/security/chkrootkit/schedule/status") +def api_sec_chk_schedule_status(): + return api_wrap(lambda: ssh_run(chkrootkit.schedule_status_cmd())) + + +@app.route("/api/security/chkrootkit/schedule/remove", methods=["POST"]) +def api_sec_chk_schedule_remove(): + return api_wrap(lambda: ssh_run(chkrootkit.schedule_remove_cmd())) + + +@app.route("/api/security/chkrootkit/uninstall", methods=["POST"]) +def api_sec_chk_uninstall(): + return api_wrap(lambda: ssh_run(chkrootkit.uninstall_cmd(), timeout=60)) + + +# ── API: Security - Lynis ─────────────────────────────────────────── +@app.route("/api/security/lynis/status") +def api_sec_lyn_status(): + return api_wrap(lambda: ssh_run(lynis.status_cmd())) + + +@app.route("/api/security/lynis/install", methods=["POST"]) +def api_sec_lyn_install(): + return api_wrap(lambda: ssh_run(lynis.install_cmd(), timeout=120)) + + +@app.route("/api/security/lynis/audit-quick", methods=["POST"]) +def api_sec_lyn_audit_quick(): + return api_wrap(lambda: ssh_run(lynis.audit_quick_cmd(), timeout=120)) + + +@app.route("/api/security/lynis/audit-full", methods=["POST"]) +def api_sec_lyn_audit_full(): + return api_wrap(lambda: ssh_run(lynis.audit_full_cmd(), timeout=120)) + + +@app.route("/api/security/lynis/hardening-index") +def api_sec_lyn_index(): + return api_wrap(lambda: ssh_run(lynis.hardening_index_cmd())) + + +@app.route("/api/security/lynis/warnings") +def api_sec_lyn_warnings(): + return api_wrap(lambda: ssh_run(lynis.show_warnings_cmd())) + + +@app.route("/api/security/lynis/suggestions") +def api_sec_lyn_suggestions(): + return api_wrap(lambda: ssh_run(lynis.show_suggestions_cmd())) + + +@app.route("/api/security/lynis/report") +def api_sec_lyn_report(): + return api_wrap(lambda: ssh_run(lynis.show_report_cmd())) + + +@app.route("/api/security/lynis/log") +def api_sec_lyn_log(): + return api_wrap(lambda: ssh_run(lynis.log_cmd())) + + +@app.route("/api/security/lynis/profile") +def api_sec_lyn_profile(): + return api_wrap(lambda: ssh_run(lynis.profile_cmd())) + + +@app.route("/api/security/lynis/schedule", methods=["POST"]) +def api_sec_lyn_schedule(): + schedule = (request.json or {}).get("schedule", "weekly") + return api_wrap(lambda: ssh_run(lynis.schedule_cmd(schedule))) + + +@app.route("/api/security/lynis/schedule/status") +def api_sec_lyn_schedule_status(): + return api_wrap(lambda: ssh_run(lynis.schedule_status_cmd())) + + +@app.route("/api/security/lynis/schedule/remove", methods=["POST"]) +def api_sec_lyn_schedule_remove(): + return api_wrap(lambda: ssh_run(lynis.schedule_remove_cmd())) + + +@app.route("/api/security/lynis/uninstall", methods=["POST"]) +def api_sec_lyn_uninstall(): + return api_wrap(lambda: ssh_run(lynis.uninstall_cmd(), timeout=60)) + + +# ── API: Security - OSSEC ─────────────────────────────────────────── +@app.route("/api/security/ossec/status") +def api_sec_osc_status(): + return api_wrap(lambda: ssh_run(ossec.status_cmd())) + + +@app.route("/api/security/ossec/install", methods=["POST"]) +def api_sec_osc_install(): + return api_wrap(lambda: ssh_run(ossec.install_cmd(), timeout=120)) + + +@app.route("/api/security/ossec/start", methods=["POST"]) +def api_sec_osc_start(): + return api_wrap(lambda: ssh_run(ossec.start_cmd())) + + +@app.route("/api/security/ossec/stop", methods=["POST"]) +def api_sec_osc_stop(): + return api_wrap(lambda: ssh_run(ossec.stop_cmd())) + + +@app.route("/api/security/ossec/restart", methods=["POST"]) +def api_sec_osc_restart(): + return api_wrap(lambda: ssh_run(ossec.restart_cmd())) + + +@app.route("/api/security/ossec/alerts") +def api_sec_osc_alerts(): + return api_wrap(lambda: ssh_run(ossec.alerts_cmd())) + + +@app.route("/api/security/ossec/alerts-today") +def api_sec_osc_alerts_today(): + return api_wrap(lambda: ssh_run(ossec.alerts_today_cmd())) + + +@app.route("/api/security/ossec/log") +def api_sec_osc_log(): + return api_wrap(lambda: ssh_run(ossec.log_cmd())) + + +@app.route("/api/security/ossec/syscheck") +def api_sec_osc_syscheck(): + return api_wrap(lambda: ssh_run(ossec.syscheck_cmd())) + + +@app.route("/api/security/ossec/config") +def api_sec_osc_config(): + return api_wrap(lambda: ssh_run(ossec.config_cmd())) + + +@app.route("/api/security/ossec/rules") +def api_sec_osc_rules(): + return api_wrap(lambda: ssh_run(ossec.rules_cmd())) + + +@app.route("/api/security/ossec/active-response") +def api_sec_osc_active_response(): + return api_wrap(lambda: ssh_run(ossec.active_response_cmd())) + + +@app.route("/api/security/ossec/agents") +def api_sec_osc_agents(): + return api_wrap(lambda: ssh_run(ossec.agent_list_cmd())) + + +@app.route("/api/security/ossec/uninstall", methods=["POST"]) +def api_sec_osc_uninstall(): + return api_wrap(lambda: ssh_run(ossec.uninstall_cmd(), timeout=60)) + + +# ── API: Security - ModSecurity ────────────────────────────────────── +@app.route("/api/security/modsec/status") +def api_sec_mod_status(): + return api_wrap(lambda: ssh_run(modsecurity.status_cmd())) + + +@app.route("/api/security/modsec/install", methods=["POST"]) +def api_sec_mod_install(): + return api_wrap(lambda: ssh_run(modsecurity.install_cmd(), timeout=120)) + + +@app.route("/api/security/modsec/enable", methods=["POST"]) +def api_sec_mod_enable(): + return api_wrap(lambda: ssh_run(modsecurity.enable_cmd())) + + +@app.route("/api/security/modsec/disable", methods=["POST"]) +def api_sec_mod_disable(): + return api_wrap(lambda: ssh_run(modsecurity.disable_cmd())) + + +@app.route("/api/security/modsec/audit-log") +def api_sec_mod_audit_log(): + return api_wrap(lambda: ssh_run(modsecurity.audit_log_cmd())) + + +@app.route("/api/security/modsec/debug-log") +def api_sec_mod_debug_log(): + return api_wrap(lambda: ssh_run(modsecurity.debug_log_cmd())) + + +@app.route("/api/security/modsec/rules") +def api_sec_mod_rules(): + return api_wrap(lambda: ssh_run(modsecurity.rules_list_cmd())) + + +@app.route("/api/security/modsec/rule/disable", methods=["POST"]) +def api_sec_mod_rule_disable(): + rule_id = (request.json or {}).get("rule_id", "") + if not rule_id: + return jsonify({"ok": False, "error": "need rule_id"}), 400 + return api_wrap(lambda: ssh_run(modsecurity.rule_disable_cmd(rule_id))) + + +@app.route("/api/security/modsec/rule/enable", methods=["POST"]) +def api_sec_mod_rule_enable(): + rule_id = (request.json or {}).get("rule_id", "") + if not rule_id: + return jsonify({"ok": False, "error": "need rule_id"}), 400 + return api_wrap(lambda: ssh_run(modsecurity.rule_enable_cmd(rule_id))) + + +@app.route("/api/security/modsec/crs-update", methods=["POST"]) +def api_sec_mod_crs_update(): + return api_wrap(lambda: ssh_run(modsecurity.crs_update_cmd(), timeout=60)) + + +@app.route("/api/security/modsec/config") +def api_sec_mod_config(): + return api_wrap(lambda: ssh_run(modsecurity.config_cmd())) + + +@app.route("/api/security/modsec/crs-config") +def api_sec_mod_crs_config(): + return api_wrap(lambda: ssh_run(modsecurity.config_crs_cmd())) + + +@app.route("/api/security/modsec/exclusions") +def api_sec_mod_exclusions(): + return api_wrap(lambda: ssh_run(modsecurity.exclusions_cmd())) + + +@app.route("/api/security/modsec/test", methods=["POST"]) +def api_sec_mod_test(): + return api_wrap(lambda: ssh_run(modsecurity.test_cmd())) + + +@app.route("/api/security/modsec/nginx-status") +def api_sec_mod_nginx_status(): + return api_wrap(lambda: ssh_run(modsecurity.nginx_status_cmd())) + + +@app.route("/api/security/modsec/uninstall", methods=["POST"]) +def api_sec_mod_uninstall(): + return api_wrap(lambda: ssh_run(modsecurity.uninstall_cmd(), timeout=60)) + + +# ── API: Security - AIDE ──────────────────────────────────────────── +@app.route("/api/security/aide/status") +def api_sec_aide_status(): + return api_wrap(lambda: ssh_run(aide.status_cmd())) + + +@app.route("/api/security/aide/install", methods=["POST"]) +def api_sec_aide_install(): + return api_wrap(lambda: ssh_run(aide.install_cmd(), timeout=120)) + + +@app.route("/api/security/aide/check", methods=["POST"]) +def api_sec_aide_check(): + return api_wrap(lambda: ssh_run(aide.check_cmd(), timeout=120)) + + +@app.route("/api/security/aide/update", methods=["POST"]) +def api_sec_aide_update(): + return api_wrap(lambda: ssh_run(aide.update_cmd(), timeout=120)) + + +@app.route("/api/security/aide/init", methods=["POST"]) +def api_sec_aide_init(): + return api_wrap(lambda: ssh_run(aide.init_cmd(), timeout=120)) + + +@app.route("/api/security/aide/compare", methods=["POST"]) +def api_sec_aide_compare(): + return api_wrap(lambda: ssh_run(aide.compare_cmd(), timeout=120)) + + +@app.route("/api/security/aide/log") +def api_sec_aide_log(): + return api_wrap(lambda: ssh_run(aide.log_cmd())) + + +@app.route("/api/security/aide/config") +def api_sec_aide_config(): + return api_wrap(lambda: ssh_run(aide.config_cmd())) + + +@app.route("/api/security/aide/rules") +def api_sec_aide_rules(): + return api_wrap(lambda: ssh_run(aide.config_rules_cmd())) + + +@app.route("/api/security/aide/schedule", methods=["POST"]) +def api_sec_aide_schedule(): + schedule = (request.json or {}).get("schedule", "daily") + return api_wrap(lambda: ssh_run(aide.schedule_cmd(schedule))) + + +@app.route("/api/security/aide/schedule/status") +def api_sec_aide_schedule_status(): + return api_wrap(lambda: ssh_run(aide.schedule_status_cmd())) + + +@app.route("/api/security/aide/schedule/remove", methods=["POST"]) +def api_sec_aide_schedule_remove(): + return api_wrap(lambda: ssh_run(aide.schedule_remove_cmd())) + + +@app.route("/api/security/aide/uninstall", methods=["POST"]) +def api_sec_aide_uninstall(): + return api_wrap(lambda: ssh_run(aide.uninstall_cmd(), timeout=60)) + + +# ── API: Security - Cowrie ────────────────────────────────────────── +@app.route("/api/security/cowrie/status") +def api_sec_cow_status(): + return api_wrap(lambda: ssh_run(cowrie.status_cmd())) + + +@app.route("/api/security/cowrie/install", methods=["POST"]) +def api_sec_cow_install(): + return api_wrap(lambda: ssh_run(cowrie.install_cmd(), timeout=120)) + + +@app.route("/api/security/cowrie/start", methods=["POST"]) +def api_sec_cow_start(): + return api_wrap(lambda: ssh_run(cowrie.start_cmd())) + + +@app.route("/api/security/cowrie/stop", methods=["POST"]) +def api_sec_cow_stop(): + return api_wrap(lambda: ssh_run(cowrie.stop_cmd())) + + +@app.route("/api/security/cowrie/restart", methods=["POST"]) +def api_sec_cow_restart(): + return api_wrap(lambda: ssh_run(cowrie.restart_cmd())) + + +@app.route("/api/security/cowrie/sessions") +def api_sec_cow_sessions(): + return api_wrap(lambda: ssh_run(cowrie.sessions_cmd())) + + +@app.route("/api/security/cowrie/top-attackers") +def api_sec_cow_top_attackers(): + return api_wrap(lambda: ssh_run(cowrie.top_attackers_cmd())) + + +@app.route("/api/security/cowrie/credentials") +def api_sec_cow_credentials(): + return api_wrap(lambda: ssh_run(cowrie.credentials_cmd())) + + +@app.route("/api/security/cowrie/downloads") +def api_sec_cow_downloads(): + return api_wrap(lambda: ssh_run(cowrie.downloads_cmd())) + + +@app.route("/api/security/cowrie/log") +def api_sec_cow_log(): + return api_wrap(lambda: ssh_run(cowrie.log_cmd())) + + +@app.route("/api/security/cowrie/log-json") +def api_sec_cow_log_json(): + return api_wrap(lambda: ssh_run(cowrie.log_json_cmd())) + + +@app.route("/api/security/cowrie/config") +def api_sec_cow_config(): + return api_wrap(lambda: ssh_run(cowrie.config_cmd())) + + +@app.route("/api/security/cowrie/port-redirect", methods=["POST"]) +def api_sec_cow_port_redirect(): + enable = (request.json or {}).get("enable", True) + return api_wrap(lambda: ssh_run(cowrie.port_redirect_cmd(enable))) + + +@app.route("/api/security/cowrie/uninstall", methods=["POST"]) +def api_sec_cow_uninstall(): + return api_wrap(lambda: ssh_run(cowrie.uninstall_cmd(), timeout=60)) + + +# ── API: Security - .sec Updates ───────────────────────────────────── +@app.route("/api/security/updates/detect") +def api_sec_updates_detect(): + return api_wrap(lambda: ssh_run(sec_updates.os_detect_cmd())) + + +@app.route("/api/security/updates/list") +def api_sec_updates_list(): + return api_wrap(lambda: ssh_run(sec_updates.list_available_cmd())) + + +@app.route("/api/security/updates/check", methods=["POST"]) +def api_sec_updates_check(): + data = request.json or {} + distro_id = data.get("distro_id", "") + version_id = data.get("version_id", "") + if not distro_id or not version_id: + return jsonify({"ok": False, "error": "need distro_id and version_id"}), 400 + return api_wrap(lambda: ssh_run(sec_updates.check_updates_cmd(distro_id, version_id))) + + +@app.route("/api/security/updates/download", methods=["POST"]) +def api_sec_updates_download(): + filename = (request.json or {}).get("filename", "") + if not filename: + return jsonify({"ok": False, "error": "need filename"}), 400 + return api_wrap(lambda: ssh_run(sec_updates.download_update_cmd(filename))) + + +@app.route("/api/security/updates/preview", methods=["POST"]) +def api_sec_updates_preview(): + filename = (request.json or {}).get("filename", "") + if not filename: + return jsonify({"ok": False, "error": "need filename"}), 400 + return api_wrap(lambda: ssh_run(sec_updates.preview_update_cmd(filename))) + + +@app.route("/api/security/updates/apply", methods=["POST"]) +def api_sec_updates_apply(): + filename = (request.json or {}).get("filename", "") + if not filename: + return jsonify({"ok": False, "error": "need filename"}), 400 + return api_wrap(lambda: ssh_run(sec_updates.parse_and_apply_cmd(filename), timeout=120)) + + +@app.route("/api/security/updates/history") +def api_sec_updates_history(): + return api_wrap(lambda: ssh_run(sec_updates.update_history_cmd())) + + +# ── API: Hosting Providers ──────────────────────────────────────── +@app.route("/api/hosting/providers") +def api_hosting_providers(): + return jsonify({"ok": True, "data": hosting.PROVIDER_LIST}) + + +# ── API: Wizard Test ───────────────────────────────────────────── +@app.route("/api/wizard/test") +def api_wizard_test(): + """Test SSH connection and optionally DNS API, return verbose results.""" + result = {"ssh_ok": False, "api_ok": False} + # SSH test + try: + ssh_res = ssh_run("echo 'SSH OK' && hostname && whoami && uptime", timeout=15) + if ssh_res.get("exit_code", -1) == 0 or ssh_res.get("stdout", ""): + result["ssh_ok"] = True + result["ssh_output"] = ssh_res.get("stdout", "") + else: + result["ssh_error"] = ssh_res.get("stderr", "") or "SSH connection failed" + except Exception as e: + result["ssh_error"] = str(e) + # DNS API test (only if provider configured) + cfg = config.load() + provider = cfg.get("hosting_provider", "") + api_key = cfg.get("hostinger_api_key", "") + if provider and api_key: + try: + domain = cfg.get("domain", "") + prov = hosting.PROVIDERS.get(provider, {}) + if prov and domain: + base = prov.get("dns_base", "") + auth_header = prov.get("auth_header", "Authorization") + auth_prefix = prov.get("auth_prefix", "Bearer ") + cmd = ( + f"curl -s -w '\\nHTTP_CODE:%{{http_code}}' " + f"-H '{auth_header}: {auth_prefix}{api_key}' " + f"-H 'Content-Type: application/json' " + f"'{base}/{domain}' 2>&1" + ) + api_res = ssh_run(cmd, timeout=15) + output = api_res.get("stdout", "") + if "HTTP_CODE:200" in output: + result["api_ok"] = True + else: + code = output.rsplit("HTTP_CODE:", 1)[-1].strip() if "HTTP_CODE:" in output else "?" + result["api_error"] = f"API returned HTTP {code}" + else: + result["api_error"] = "Provider or domain not configured" + except Exception as e: + result["api_error"] = str(e) + else: + result["api_error"] = "No DNS provider configured (optional)" + return jsonify({"ok": True, "data": result}) + + +# ── Run ────────────────────────────────────────────────────────────── +if __name__ == "__main__": + cfg = config.load() + print(f"SETEC LABS Manager starting on http://localhost:{cfg.get('flask_port', 5000)}") + app.run(host="127.0.0.1", port=cfg.get("flask_port", 5000), debug=False) diff --git a/setec-web/audit.py b/setec-web/audit.py new file mode 100644 index 0000000..77e90f6 --- /dev/null +++ b/setec-web/audit.py @@ -0,0 +1,69 @@ +"""Audit logging — log all actions with timestamp, user, action, target.""" + +import os +import json +import time +from datetime import datetime + +AUDIT_DIR = os.path.join(os.path.expanduser("~"), ".setec-mgr") +AUDIT_FILE = os.path.join(AUDIT_DIR, "audit.log") +MAX_LOG_SIZE = 5 * 1024 * 1024 # 5MB, then rotate + + +def log(action, target="", details="", user="admin", ip=""): + """Append an audit entry.""" + os.makedirs(AUDIT_DIR, exist_ok=True) + _rotate_if_needed() + entry = { + "ts": datetime.utcnow().isoformat() + "Z", + "user": user, + "ip": ip, + "action": action, + "target": target, + "details": details, + } + with open(AUDIT_FILE, "a", encoding="utf-8") as f: + f.write(json.dumps(entry) + "\n") + + +def _rotate_if_needed(): + """Rotate log if it exceeds MAX_LOG_SIZE.""" + if os.path.exists(AUDIT_FILE) and os.path.getsize(AUDIT_FILE) > MAX_LOG_SIZE: + rotated = AUDIT_FILE + f".{int(time.time())}" + os.rename(AUDIT_FILE, rotated) + + +def get_recent(count=100): + """Return the last N audit entries.""" + if not os.path.exists(AUDIT_FILE): + return [] + with open(AUDIT_FILE, "r", encoding="utf-8") as f: + lines = f.readlines() + entries = [] + for line in lines[-count:]: + try: + entries.append(json.loads(line.strip())) + except json.JSONDecodeError: + pass + return entries + + +def search(query="", action_filter="", limit=200): + """Search audit log.""" + if not os.path.exists(AUDIT_FILE): + return [] + results = [] + with open(AUDIT_FILE, "r", encoding="utf-8") as f: + for line in f: + try: + entry = json.loads(line.strip()) + if action_filter and entry.get("action") != action_filter: + continue + if query and query.lower() not in line.lower(): + continue + results.append(entry) + if len(results) >= limit: + break + except json.JSONDecodeError: + pass + return results diff --git a/setec-web/backup.py b/setec-web/backup.py new file mode 100644 index 0000000..6b03815 --- /dev/null +++ b/setec-web/backup.py @@ -0,0 +1,179 @@ +# Backup & recovery commands for encrypted remote backups +# Each function returns a bash command string that app.py executes via ssh_run() + + +def backup_now_cmd(paths="/etc /var/www /opt/seteclabs /root", dest="/var/backups/setec", + encrypt_pass="", remote_host="", remote_path=""): + """Return bash cmd to create an encrypted compressed backup.""" + ts = "$(date +%Y%m%d_%H%M%S)" + archive = f"{dest}/setec-backup-{ts}.tar.gz" + + cmd = ( + f"echo '=== Creating Backup ===' && " + f"mkdir -p {dest} && " + f"tar czf {archive} {paths} 2>&1 && " + f"SIZE=$(du -h {archive} | cut -f1) && " + f"echo \"Archive created: {archive} ($SIZE)\" " + ) + + if encrypt_pass: + enc_archive = f"{archive}.enc" + cmd += ( + f"&& echo 'Encrypting...' && " + f"openssl enc -aes-256-cbc -salt -pbkdf2 -in {archive} " + f"-out {enc_archive} -pass pass:'{encrypt_pass}' 2>&1 && " + f"rm -f {archive} && " + f"echo 'Encrypted: {enc_archive}' " + ) + archive = enc_archive + + if remote_host and remote_path: + cmd += ( + f"&& echo 'Transferring to remote...' && " + f"scp -o StrictHostKeyChecking=no '{archive}' " + f"'{remote_host}:{remote_path}/' 2>&1 && " + f"echo 'Transferred to {remote_host}:{remote_path}' " + ) + + cmd += "&& echo '' && echo 'Backup complete'" + return cmd + + +def backup_list_cmd(dest="/var/backups/setec"): + """Return bash cmd to list existing backups.""" + return ( + f"echo '=== Backup Archive List ===' && " + f"if [ -d {dest} ]; then " + f" ls -lhS {dest}/setec-backup-* 2>/dev/null | head -30 || echo 'No backups found'; " + f" echo '' && " + f" echo '--- Disk Usage ---' && " + f" du -sh {dest} 2>/dev/null; " + f"else " + f" echo 'Backup directory {dest} does not exist'; " + f"fi" + ) + + +def backup_restore_cmd(archive, dest="/", encrypt_pass=""): + """Return bash cmd to restore from a backup archive.""" + if encrypt_pass: + return ( + f"echo '=== Restoring from Encrypted Backup ===' && " + f"DECRYPTED=$(mktemp /tmp/setec-restore-XXXXXX.tar.gz) && " + f"openssl enc -aes-256-cbc -d -salt -pbkdf2 -in '{archive}' " + f"-out \"$DECRYPTED\" -pass pass:'{encrypt_pass}' 2>&1 && " + f"echo 'Decrypted successfully' && " + f"tar xzf \"$DECRYPTED\" -C {dest} 2>&1 && " + f"rm -f \"$DECRYPTED\" && " + f"echo 'Restore complete from {archive}'" + ) + return ( + f"echo '=== Restoring from Backup ===' && " + f"tar xzf '{archive}' -C {dest} 2>&1 && " + f"echo 'Restore complete from {archive}'" + ) + + +def backup_delete_cmd(archive): + """Return bash cmd to delete a backup archive.""" + return ( + f"echo '=== Deleting Backup ===' && " + f"rm -f '{archive}' 2>&1 && " + f"echo 'Deleted: {archive}'" + ) + + +def backup_schedule_cmd(paths="/etc /var/www /opt/seteclabs /root", dest="/var/backups/setec", + encrypt_pass="", schedule="daily", keep=7): + """Return bash cmd to set up scheduled backups via cron.""" + if schedule == "hourly": + cron_time = "0 * * * *" + elif schedule == "daily": + cron_time = "0 2 * * *" + elif schedule == "weekly": + cron_time = "0 2 * * 0" + else: + cron_time = "0 2 * * *" + + encrypt_section = "" + if encrypt_pass: + encrypt_section = ( + f"openssl enc -aes-256-cbc -salt -pbkdf2 -in \\\"$ARCHIVE\\\" " + f"-out \\\"$ARCHIVE.enc\\\" -pass pass:'{encrypt_pass}' && " + f"rm -f \\\"$ARCHIVE\\\" && " + f"ARCHIVE=\\\"$ARCHIVE.enc\\\" && " + ) + + script = ( + "#!/bin/bash\\n" + "# Setec Labs Automated Backup\\n" + f"DEST={dest}\\n" + "TS=$(date +%Y%m%d_%H%M%S)\\n" + "ARCHIVE=$DEST/setec-backup-$TS.tar.gz\\n" + f"KEEP={keep}\\n" + "\\n" + "mkdir -p $DEST\\n" + f"tar czf \\\"$ARCHIVE\\\" {paths} 2>/dev/null\\n" + f"{encrypt_section}" + "echo \\\"$(date): Backup created: $ARCHIVE\\\" >> /var/log/setec-backup.log\\n" + "\\n" + "# Cleanup old backups\\n" + "ls -1t $DEST/setec-backup-* 2>/dev/null | tail -n +$((KEEP+1)) | xargs rm -f 2>/dev/null\\n" + ) + + return ( + "echo '=== Setting Up Scheduled Backups ===' && " + f"mkdir -p {dest} && " + f"echo -e '{script}' > /usr/local/bin/setec-backup.sh && " + "chmod +x /usr/local/bin/setec-backup.sh && " + "touch /var/log/setec-backup.log && " + "(crontab -l 2>/dev/null | grep -v 'setec-backup.sh'; " + f"echo '{cron_time} /usr/local/bin/setec-backup.sh') | crontab - && " + f"echo 'Backup schedule: {schedule}' && " + f"echo 'Paths: {paths}' && " + f"echo 'Destination: {dest}' && " + f"echo 'Retention: {keep} backups' && " + f"echo 'Encryption: {'yes' if encrypt_pass else 'no'}'" + ) + + +def backup_schedule_status_cmd(): + """Return bash cmd to check backup schedule status.""" + return ( + "echo '=== Backup Schedule Status ===' && " + "echo '' && " + "echo '--- Script ---' && " + "if [ -f /usr/local/bin/setec-backup.sh ]; then " + " echo 'Script: INSTALLED'; " + " ls -la /usr/local/bin/setec-backup.sh; " + "else " + " echo 'Script: NOT INSTALLED'; " + "fi && " + "echo '' && " + "echo '--- Cron Job ---' && " + "if crontab -l 2>/dev/null | grep -q 'setec-backup.sh'; then " + " echo 'Cron: ACTIVE'; " + " crontab -l 2>/dev/null | grep 'setec-backup.sh'; " + "else " + " echo 'Cron: NOT FOUND'; " + "fi && " + "echo '' && " + "echo '--- Recent Backup Log (last 10) ---' && " + "if [ -f /var/log/setec-backup.log ]; then " + " tail -10 /var/log/setec-backup.log; " + "else " + " echo 'No backup log found'; " + "fi" + ) + + +def backup_schedule_remove_cmd(): + """Return bash cmd to remove scheduled backups.""" + return ( + "echo '=== Removing Scheduled Backups ===' && " + "(crontab -l 2>/dev/null | grep -v 'setec-backup.sh') | crontab - && " + "rm -f /usr/local/bin/setec-backup.sh && " + "echo 'Backup script removed' && " + "echo 'Cron job removed' && " + "echo 'Existing backups preserved'" + ) diff --git a/setec-web/chkrootkit.py b/setec-web/chkrootkit.py new file mode 100644 index 0000000..7ff7d5b --- /dev/null +++ b/setec-web/chkrootkit.py @@ -0,0 +1,106 @@ +""" +Command-builder module for managing chkrootkit on a Linux VPS. +Each function returns a bash command string. +""" + + +def status_cmd() -> str: + """Check if chkrootkit is installed and show version.""" + return ( + "if command -v chkrootkit >/dev/null 2>&1; then " + "echo 'chkrootkit is installed'; chkrootkit -V 2>&1; " + "dpkg -s chkrootkit 2>/dev/null | grep -E '^(Package|Version|Status):'; " + "else echo 'chkrootkit is NOT installed'; fi" + ) + + +def install_cmd() -> str: + """Install chkrootkit via apt.""" + return "apt-get update && apt-get install -y chkrootkit" + + +def check_cmd() -> str: + """Run a full chkrootkit scan, filtering out common noise.""" + return ( + "chkrootkit 2>&1 | grep -v " + "'^Checking' | grep -v '^ROOTDIR' | grep -v '^nothing found' | " + "grep -v '^not infected' | grep -v '^not tested' | " + "grep -v '^\\.\\.\\.'" + " || echo 'Scan complete — no suspicious findings.'" + ) + + +def check_expert_cmd() -> str: + """Run chkrootkit in expert mode for detailed output.""" + return "chkrootkit -x 2>&1" + + +def log_cmd(lines: int = 50) -> str: + """View recent chkrootkit log entries.""" + return ( + "if [ -f /var/log/chkrootkit/log.today ]; then " + f"tail -n {int(lines)} /var/log/chkrootkit/log.today; " + "elif [ -f /var/log/chkrootkit.log ]; then " + f"tail -n {int(lines)} /var/log/chkrootkit.log; " + "else echo 'No chkrootkit log found. Check /etc/chkrootkit.conf for LOG_DIR.'; fi" + ) + + +def schedule_cmd(schedule: str = "daily") -> str: + """Set up a cron job for chkrootkit scans (daily or weekly).""" + cron_file = "/etc/cron.d/chkrootkit-scan" + if schedule == "weekly": + cron_expr = "0 3 * * 0" + else: + cron_expr = "0 3 * * *" + return ( + f"echo '{cron_expr} root /usr/sbin/chkrootkit > " + f"/var/log/chkrootkit.log 2>&1' > {cron_file} && " + f"chmod 644 {cron_file} && " + f"echo 'chkrootkit scheduled {schedule} via {cron_file}'" + ) + + +def schedule_status_cmd() -> str: + """Show the current chkrootkit cron schedule.""" + return ( + "echo '=== /etc/cron.d ===' && " + "grep -rl chkrootkit /etc/cron.d/ 2>/dev/null && " + "cat /etc/cron.d/chkrootkit-scan 2>/dev/null; " + "echo '=== /etc/cron.daily ===' && " + "ls -la /etc/cron.daily/chkrootkit 2>/dev/null; " + "echo '=== crontab ===' && " + "crontab -l 2>/dev/null | grep chkrootkit || " + "echo 'No chkrootkit cron entries found.'" + ) + + +def schedule_remove_cmd() -> str: + """Remove chkrootkit cron entries.""" + return ( + "rm -f /etc/cron.d/chkrootkit-scan && " + "echo 'Removed /etc/cron.d/chkrootkit-scan (if it existed)'" + ) + + +def config_cmd() -> str: + """Show chkrootkit configuration.""" + return ( + "if [ -f /etc/chkrootkit.conf ]; then " + "echo '=== /etc/chkrootkit.conf ===' && cat /etc/chkrootkit.conf; " + "elif [ -f /etc/chkrootkit/chkrootkit.conf ]; then " + "echo '=== /etc/chkrootkit/chkrootkit.conf ===' && " + "cat /etc/chkrootkit/chkrootkit.conf; " + "else echo 'No chkrootkit config file found.'; fi && " + "echo && echo '=== Defaults (if present) ===' && " + "cat /etc/default/chkrootkit 2>/dev/null || true" + ) + + +def uninstall_cmd() -> str: + """Remove chkrootkit and clean up.""" + return ( + "apt-get remove --purge -y chkrootkit && " + "rm -f /etc/cron.d/chkrootkit-scan && " + "echo 'chkrootkit removed.'" + ) diff --git a/setec-web/clamav.py b/setec-web/clamav.py new file mode 100644 index 0000000..bd491e2 --- /dev/null +++ b/setec-web/clamav.py @@ -0,0 +1,187 @@ +# ClamAV antivirus management commands +# Each function returns a bash command string that app.py executes via ssh_run() + + +def status_cmd(): + """Return bash cmd to check ClamAV install and service status.""" + return ( + "echo '=== ClamAV Installation ===' && " + "dpkg -l | grep clamav | awk '{print $2, $3}' 2>/dev/null || echo 'ClamAV not installed' && " + "echo '' && echo '=== Service Status ===' && " + "systemctl is-active clamav-daemon 2>/dev/null && echo 'clamd: running' || echo 'clamd: not running' && " + "systemctl is-active clamav-freshclam 2>/dev/null && echo 'freshclam: running' || echo 'freshclam: not running' && " + "echo '' && echo '=== Virus DB ===' && " + "if [ -f /var/lib/clamav/daily.cld ] || [ -f /var/lib/clamav/daily.cvd ]; then " + " ls -lh /var/lib/clamav/*.{cld,cvd} 2>/dev/null; " + " sigtool --info /var/lib/clamav/daily.cld 2>/dev/null | grep -E 'Version|Sigs|Build' || " + " sigtool --info /var/lib/clamav/daily.cvd 2>/dev/null | grep -E 'Version|Sigs|Build'; " + "else " + " echo 'No virus database found'; " + "fi && " + "echo '' && echo '=== ClamAV Version ===' && " + "clamscan --version 2>/dev/null || echo 'clamscan not found'" + ) + + +def install_cmd(): + """Return bash cmd to install ClamAV and enable services.""" + return ( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y clamav clamav-daemon clamav-freshclam 2>&1 && " + "systemctl stop clamav-freshclam 2>/dev/null; " + "freshclam 2>&1; " + "systemctl enable clamav-daemon clamav-freshclam 2>&1 && " + "systemctl start clamav-freshclam 2>&1 && " + "systemctl start clamav-daemon 2>&1 && " + "echo 'ClamAV installed and services started'" + ) + + +def update_defs_cmd(): + """Return bash cmd to update virus definitions.""" + return ( + "systemctl stop clamav-freshclam 2>/dev/null; " + "freshclam 2>&1; " + "systemctl start clamav-freshclam 2>&1 && " + "echo '' && echo '=== Updated DB Info ===' && " + "ls -lh /var/lib/clamav/*.{cld,cvd} 2>/dev/null" + ) + + +def scan_cmd(path, recursive=True): + """Return bash cmd to scan a path with clamscan.""" + flags = "-ri" if recursive else "-i" + return ( + f"echo '=== Scanning: {path} ===' && " + f"echo 'Started: '$(date) && " + f"clamscan {flags} --no-summary '{path}' 2>&1; " + f"clamscan {flags} '{path}' 2>&1 | tail -8 && " + f"echo 'Finished: '$(date)" + ) + + +def scan_quick_cmd(): + """Return bash cmd for a quick scan of common attack targets.""" + return ( + "echo '=== Quick Scan: /tmp /var/tmp /dev/shm /var/www /home ===' && " + "echo 'Started: '$(date) && " + "clamscan -ri --no-summary /tmp /var/tmp /dev/shm /var/www /home 2>&1; " + "clamscan -ri /tmp /var/tmp /dev/shm /var/www /home 2>&1 | tail -10 && " + "echo 'Finished: '$(date)" + ) + + +def scan_full_cmd(): + """Return bash cmd for full system scan (excludes /proc /sys /dev).""" + return ( + "echo '=== Full System Scan ===' && " + "echo 'Started: '$(date) && " + "clamscan -ri --exclude-dir='^/proc' --exclude-dir='^/sys' " + "--exclude-dir='^/dev' --exclude-dir='^/run' " + "--log=/var/log/clamav/lastscan.log / 2>&1 | tail -15 && " + "echo 'Finished: '$(date)" + ) + + +def log_cmd(lines=50): + """Return bash cmd to view ClamAV scan logs.""" + return ( + "echo '=== Last Scan Log ===' && " + f"tail -{lines} /var/log/clamav/lastscan.log 2>/dev/null || echo 'No scan log found' && " + "echo '' && echo '=== Freshclam Log ===' && " + f"tail -20 /var/log/clamav/freshclam.log 2>/dev/null || echo 'No freshclam log found'" + ) + + +def quarantine_list_cmd(): + """Return bash cmd to list quarantined files.""" + return ( + "echo '=== Quarantine ===' && " + "if [ -d /var/lib/clamav/quarantine ]; then " + " ls -lhR /var/lib/clamav/quarantine 2>/dev/null; " + " echo '' && echo \"Total: $(find /var/lib/clamav/quarantine -type f | wc -l) files\"; " + "else " + " echo 'No quarantine directory (clean system)'; " + "fi" + ) + + +def quarantine_scan_cmd(path, recursive=True): + """Return bash cmd to scan and move infected files to quarantine.""" + flags = "-ri" if recursive else "-i" + return ( + "mkdir -p /var/lib/clamav/quarantine && " + f"echo '=== Scan + Quarantine: {path} ===' && " + f"clamscan {flags} --move=/var/lib/clamav/quarantine " + f"--log=/var/log/clamav/lastscan.log '{path}' 2>&1 | tail -15" + ) + + +def quarantine_delete_cmd(): + """Return bash cmd to purge all quarantined files.""" + return ( + "if [ -d /var/lib/clamav/quarantine ]; then " + " count=$(find /var/lib/clamav/quarantine -type f | wc -l) && " + " rm -rf /var/lib/clamav/quarantine/* && " + " echo \"Purged $count quarantined files\"; " + "else " + " echo 'No quarantine directory'; " + "fi" + ) + + +def schedule_cmd(schedule="daily", paths="/"): + """Return bash cmd to set up a cron job for scheduled scanning.""" + if schedule == "daily": + cron_time = "0 3 * * *" + elif schedule == "weekly": + cron_time = "0 3 * * 0" + elif schedule == "monthly": + cron_time = "0 3 1 * *" + else: + cron_time = "0 3 * * *" + return ( + f"(crontab -l 2>/dev/null | grep -v 'setec-clamscan'; " + f"echo '{cron_time} clamscan -ri --exclude-dir=\"^/proc\" --exclude-dir=\"^/sys\" " + f"--exclude-dir=\"^/dev\" --exclude-dir=\"^/run\" " + f"--move=/var/lib/clamav/quarantine --log=/var/log/clamav/lastscan.log " + f"{paths} # setec-clamscan') | crontab - 2>&1 && " + f"echo 'Scheduled {schedule} scan of {paths}' && " + f"crontab -l | grep setec-clamscan" + ) + + +def schedule_status_cmd(): + """Return bash cmd to show current scan schedule.""" + return ( + "echo '=== Scan Schedule ===' && " + "crontab -l 2>/dev/null | grep setec-clamscan || echo 'No scheduled scan'" + ) + + +def schedule_remove_cmd(): + """Return bash cmd to remove scheduled scan.""" + return ( + "(crontab -l 2>/dev/null | grep -v 'setec-clamscan') | crontab - 2>&1 && " + "echo 'Scheduled scan removed'" + ) + + +def config_cmd(): + """Return bash cmd to show ClamAV config.""" + return ( + "echo '=== clamd.conf ===' && " + "cat /etc/clamav/clamd.conf 2>/dev/null || echo 'Not found' && " + "echo '' && echo '=== freshclam.conf ===' && " + "cat /etc/clamav/freshclam.conf 2>/dev/null || echo 'Not found'" + ) + + +def uninstall_cmd(): + """Return bash cmd to remove ClamAV.""" + return ( + "systemctl stop clamav-daemon clamav-freshclam 2>/dev/null; " + "DEBIAN_FRONTEND=noninteractive apt-get remove --purge -y clamav clamav-daemon clamav-freshclam 2>&1 && " + "apt-get autoremove -y 2>&1 && " + "echo 'ClamAV uninstalled'" + ) diff --git a/setec-web/config.py b/setec-web/config.py new file mode 100644 index 0000000..fc6906b --- /dev/null +++ b/setec-web/config.py @@ -0,0 +1,61 @@ +import os +import json + +CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".setec-mgr", "config.json") + +DEFAULTS = { + "vps_host": "", + "vps_user": "root", + "vps_port": 22, + "ssh_key_path": "", + "domain": "", + "hosting_provider": "", + "hostinger_api_key": "", + "web_root": "/var/www", + "compose_path": "/opt/seteclabs/docker-compose.yml", + "flask_port": 5000, + "flask_secret": "", + "setup_complete": False, + "tos_accepted": False, + "e2e_enabled": False, +} + +# Sensitive fields that should be masked in API responses +SENSITIVE_FIELDS = {"hostinger_api_key", "flask_secret", "tunnel_key"} + + +def load(): + try: + with open(CONFIG_PATH) as f: + cfg = json.load(f) + for k, v in DEFAULTS.items(): + cfg.setdefault(k, v) + # Generate and persist flask_secret on first load if empty + if not cfg["flask_secret"]: + cfg["flask_secret"] = os.urandom(32).hex() + save(cfg) + return cfg + except (FileNotFoundError, json.JSONDecodeError): + defaults = dict(DEFAULTS) + defaults["flask_secret"] = os.urandom(32).hex() + save(defaults) + return defaults + + +def save(cfg): + os.makedirs(os.path.dirname(CONFIG_PATH), exist_ok=True) + with open(CONFIG_PATH, "w") as f: + json.dump(cfg, f, indent=2) + + +def safe_config(): + """Return config with sensitive fields masked.""" + cfg = load() + safe = dict(cfg) + for field in SENSITIVE_FIELDS: + val = safe.get(field, "") + if val and len(val) > 8: + safe[field] = val[:8] + "..." + elif val: + safe[field] = "***" + return safe diff --git a/setec-web/cowrie.py b/setec-web/cowrie.py new file mode 100644 index 0000000..2dec217 --- /dev/null +++ b/setec-web/cowrie.py @@ -0,0 +1,175 @@ +""" +Command-builder module for managing Cowrie SSH/Telnet honeypot on a Linux VPS. +Each function returns a bash command string. Cowrie installs to /opt/cowrie/cowrie-git, +runs as user 'cowrie', uses a systemd service. +""" + +COWRIE_DIR = "/opt/cowrie/cowrie-git" +COWRIE_USER = "cowrie" +COWRIE_SERVICE = "cowrie" +COWRIE_LOG = f"{COWRIE_DIR}/var/log/cowrie/cowrie.log" +COWRIE_JSON = f"{COWRIE_DIR}/var/log/cowrie/cowrie.json" +COWRIE_CFG = f"{COWRIE_DIR}/etc/cowrie.cfg" +COWRIE_DOWNLOADS = f"{COWRIE_DIR}/var/lib/cowrie/downloads" +LISTEN_PORT = 2222 + +SYSTEMD_UNIT = f"""\ +[Unit] +Description=Cowrie SSH/Telnet Honeypot +After=network.target + +[Service] +Type=simple +User={COWRIE_USER} +Group={COWRIE_USER} +WorkingDirectory={COWRIE_DIR} +ExecStart={COWRIE_DIR}/cowrie-env/bin/python3 {COWRIE_DIR}/src/cowrie/scripts/cowrie -n +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +""" + + +def status_cmd() -> str: + return ( + f"systemctl status {COWRIE_SERVICE} --no-pager; " + f"echo '---'; " + f"ss -tlnp | grep :{LISTEN_PORT}; " + f"echo '---'; " + f"systemctl show {COWRIE_SERVICE} --property=ActiveEnterTimestamp --no-pager" + ) + + +def install_cmd() -> str: + unit_content = SYSTEMD_UNIT.replace("\n", "\\n") + return ( + # Install dependencies + "apt-get update && " + "apt-get install -y git python3 python3-venv python3-dev " + "libssl-dev libffi-dev build-essential libpython3-dev " + "python3-minimal authbind virtualenv && " + # Create cowrie user + f"id -u {COWRIE_USER} &>/dev/null || " + f"useradd -r -m -d /opt/cowrie -s /bin/false {COWRIE_USER} && " + # Clone cowrie + f"mkdir -p /opt/cowrie && " + f"git clone https://github.com/cowrie/cowrie.git {COWRIE_DIR} && " + f"chown -R {COWRIE_USER}:{COWRIE_USER} /opt/cowrie && " + # Create venv and install + f"cd {COWRIE_DIR} && " + f"python3 -m venv {COWRIE_DIR}/cowrie-env && " + f"{COWRIE_DIR}/cowrie-env/bin/pip install --upgrade pip && " + f"{COWRIE_DIR}/cowrie-env/bin/pip install -r {COWRIE_DIR}/requirements.txt && " + # Set listen port in config + f"cp {COWRIE_CFG}.dist {COWRIE_CFG} && " + f"sed -i 's/^#\\?\\s*listen_endpoints\\s*=.*/listen_endpoints = tcp:{LISTEN_PORT}:interface=0.0.0.0/' {COWRIE_CFG} && " + f"chown -R {COWRIE_USER}:{COWRIE_USER} /opt/cowrie && " + # Create systemd service + f"printf '{unit_content}' > /etc/systemd/system/{COWRIE_SERVICE}.service && " + f"systemctl daemon-reload && " + f"systemctl enable {COWRIE_SERVICE} && " + f"systemctl start {COWRIE_SERVICE}" + ) + + +def start_cmd() -> str: + return f"systemctl start {COWRIE_SERVICE}" + + +def stop_cmd() -> str: + return f"systemctl stop {COWRIE_SERVICE}" + + +def restart_cmd() -> str: + return f"systemctl restart {COWRIE_SERVICE}" + + +def log_cmd(lines: int = 100) -> str: + return f"tail -n {lines} {COWRIE_LOG}" + + +def log_json_cmd(lines: int = 50) -> str: + return f"tail -n {lines} {COWRIE_JSON}" + + +def sessions_cmd() -> str: + return ( + f"cat {COWRIE_JSON} | " + "jq -r 'select(.eventid == \"cowrie.session.connect\" or " + ".eventid == \"cowrie.command.input\") | " + "[.timestamp, .src_ip // empty, .input // \"[connect]\", .session] | @tsv' | " + "tail -n 200 | column -t -s $'\\t'" + ) + + +def top_attackers_cmd() -> str: + return ( + f"cat {COWRIE_JSON} | " + "jq -r 'select(.eventid == \"cowrie.session.connect\") | .src_ip' | " + "sort | uniq -c | sort -rn | head -25" + ) + + +def credentials_cmd() -> str: + return ( + f"cat {COWRIE_JSON} | " + "jq -r 'select(.eventid == \"cowrie.login.success\" or " + ".eventid == \"cowrie.login.failed\") | " + "[.timestamp, .username, .password, .src_ip, .eventid] | @tsv' | " + "tail -n 200 | column -t -s $'\\t'" + ) + + +def downloads_cmd() -> str: + return ( + f"echo '=== Downloaded files ===' && " + f"ls -lhtr {COWRIE_DOWNLOADS}/ 2>/dev/null || echo 'No downloads directory'; " + f"echo '---'; " + f"cat {COWRIE_JSON} | " + "jq -r 'select(.eventid == \"cowrie.session.file_download\") | " + "[.timestamp, .url, .shasum, .src_ip] | @tsv' | " + "tail -n 100 | column -t -s $'\\t'" + ) + + +def config_cmd() -> str: + return f"cat {COWRIE_CFG}" + + +def config_save_cmd(content: str) -> str: + escaped = content.replace("'", "'\\''") + return ( + f"cp {COWRIE_CFG} {COWRIE_CFG}.bak.$(date +%Y%m%d%H%M%S) && " + f"cat > {COWRIE_CFG} << 'COWRIE_CFG_EOF'\n{content}\nCOWRIE_CFG_EOF\n" + f"chown {COWRIE_USER}:{COWRIE_USER} {COWRIE_CFG} && " + f"systemctl restart {COWRIE_SERVICE}" + ) + + +def port_redirect_cmd(enable: bool = True) -> str: + if enable: + return ( + "iptables -t nat -A PREROUTING -p tcp --dport 22 " + f"-j REDIRECT --to-port {LISTEN_PORT} && " + "echo 'Port 22 -> 2222 redirect enabled'" + ) + else: + return ( + "iptables -t nat -D PREROUTING -p tcp --dport 22 " + f"-j REDIRECT --to-port {LISTEN_PORT} && " + "echo 'Port 22 -> 2222 redirect removed'" + ) + + +def uninstall_cmd() -> str: + return ( + f"systemctl stop {COWRIE_SERVICE}; " + f"systemctl disable {COWRIE_SERVICE}; " + f"rm -f /etc/systemd/system/{COWRIE_SERVICE}.service && " + f"systemctl daemon-reload && " + f"rm -rf /opt/cowrie && " + f"userdel -r {COWRIE_USER} 2>/dev/null; " + "echo 'Cowrie uninstalled'" + ) diff --git a/setec-web/csf.py b/setec-web/csf.py new file mode 100644 index 0000000..bfbd31f --- /dev/null +++ b/setec-web/csf.py @@ -0,0 +1,134 @@ +""" +Command-builder module for ConfigServer Security & Firewall (CSF). + +Each function returns a bash command string suitable for execution on a Linux VPS. +CSF installs to /etc/csf/. +""" + + +def status_cmd() -> str: + """Check if CSF is installed, show version and rule summary.""" + return ( + "if [ -x /usr/sbin/csf ]; then " + "echo '=== CSF Version ===' && csf -v && " + "echo '=== Rule Summary ===' && csf -l | head -60; " + "else echo 'CSF is not installed'; fi" + ) + + +def install_cmd() -> str: + """Download and install CSF from configserver.com (requires perl, iptables).""" + return ( + "apt-get install -y perl iptables libwww-perl && " + "cd /tmp && " + "rm -rf csf csf.tgz && " + "wget https://download.configserver.com/csf.tgz && " + "tar -xzf csf.tgz && " + "cd csf && " + "sh install.sh && " + "rm -rf /tmp/csf /tmp/csf.tgz" + ) + + +def start_cmd() -> str: + """Start CSF firewall.""" + return "csf -s" + + +def stop_cmd() -> str: + """Flush/stop all CSF firewall rules.""" + return "csf -f" + + +def restart_cmd() -> str: + """Restart CSF firewall.""" + return "csf -r" + + +def list_cmd() -> str: + """List all current firewall rules.""" + return "csf -l" + + +def allow_ip_cmd(ip: str, comment: str = "") -> str: + """Allow an IP address through the firewall.""" + if comment: + return f"csf -a {ip} {comment}" + return f"csf -a {ip}" + + +def deny_ip_cmd(ip: str, comment: str = "") -> str: + """Deny/block an IP address.""" + if comment: + return f"csf -d {ip} {comment}" + return f"csf -d {ip}" + + +def remove_ip_cmd(ip: str) -> str: + """Remove an IP from both allow and deny lists.""" + return f"csf -ar {ip} && csf -dr {ip}" + + +def allow_port_cmd(port: int, protocol: str = "tcp", direction: str = "in") -> str: + """Add a port to the appropriate directive in csf.conf, then restart.""" + directive = f"{protocol.upper()}_{direction.upper()}" + return ( + f"if grep -q '^{directive}' /etc/csf/csf.conf; then " + f"sed -i 's/^{directive} = \"\\(.*\\)\"/'{directive}' = \"\\1,{port}\"/' /etc/csf/csf.conf && " + f"csf -r; " + f"else echo 'Directive {directive} not found in csf.conf'; fi" + ) + + +def deny_port_cmd(port: int, protocol: str = "tcp", direction: str = "in") -> str: + """Remove a port from the appropriate directive in csf.conf, then restart.""" + directive = f"{protocol.upper()}_{direction.upper()}" + return ( + f"sed -i 's/,{port},/,/g; s/,{port}\"/\"/g; s/\"{port},/\"/g; s/\"{port}\"/\"\"/g' " + f"/etc/csf/csf.conf && csf -r" + ) + + +def temp_allow_cmd(ip: str, ttl: int = 3600) -> str: + """Temporarily allow an IP for a given number of seconds.""" + return f"csf -ta {ip} {ttl}" + + +def temp_deny_cmd(ip: str, ttl: int = 3600) -> str: + """Temporarily deny an IP for a given number of seconds.""" + return f"csf -td {ip} {ttl}" + + +def temp_list_cmd() -> str: + """Show all temporary allow/deny rules.""" + return "csf -t" + + +def grep_ip_cmd(ip: str) -> str: + """Search all firewall rules for a specific IP.""" + return f"csf -g {ip}" + + +def config_cmd() -> str: + """Display key CSF configuration directives.""" + return ( + "grep -E '^(TCP_IN|TCP_OUT|UDP_IN|UDP_OUT|TCP6_IN|TCP6_OUT|UDP6_IN|UDP6_OUT|" + "TESTING|AUTO_UPDATES|SYSLOG|RESTRICT_SYSLOG|LF_ALERT_TO|LF_DSHIELD|" + "LF_SPAMHAUS|LF_DIRWATCH|LF_INTEGRITY|LF_PARSE|CT_LIMIT|PORTFLOOD|" + "SYNFLOOD|CONNLIMIT|PORTKNOCKING|CC_DENY|CC_ALLOW) ' /etc/csf/csf.conf" + ) + + +def log_cmd(lines: int = 50) -> str: + """Tail the LFD log file.""" + return f"tail -n {lines} /var/log/lfd.log" + + +def test_cmd() -> str: + """Test iptables modules required by CSF.""" + return "csf --test" + + +def uninstall_cmd() -> str: + """Uninstall CSF.""" + return "/etc/csf/uninstall.sh" diff --git a/setec-web/ddos.py b/setec-web/ddos.py new file mode 100644 index 0000000..08fec21 --- /dev/null +++ b/setec-web/ddos.py @@ -0,0 +1,464 @@ +# DDoS/DoS detection, prevention, mitigation, rate limiting, and Cloudflare integration +# Each function returns a bash command string that app.py executes via ssh_run() + + +def connection_stats_cmd(): + """Return bash cmd to show current connection statistics.""" + return ( + "echo '=== Connection Statistics ===' && " + "echo '' && " + "echo '--- Connection States ---' && " + "ss -ant 2>/dev/null | awk 'NR>1 {state[$1]++} END {for (s in state) printf \" %-15s %d\\n\", s, state[s]}' && " + "echo '' && " + "TOTAL=$(ss -ant 2>/dev/null | tail -n +2 | wc -l) && " + "echo \"Total connections: $TOTAL\" && " + "echo '' && " + "echo '--- Top 20 IPs by Connection Count ---' && " + "ss -ant 2>/dev/null | awk 'NR>1 {split($5,a,\":\"); print a[1]}' | " + "grep -v '^$' | grep -v '\\*' | sort | uniq -c | sort -rn | head -20 && " + "echo '' && " + "echo '--- Connections per Port ---' && " + "ss -ant 2>/dev/null | awk 'NR>1 {split($4,a,\":\"); print a[length(a)]}' | " + "sort -n | uniq -c | sort -rn | head -20" + ) + + +def syn_flood_check_cmd(): + """Return bash cmd to detect potential SYN flood.""" + return ( + "echo '=== SYN Flood Detection ===' && " + "echo '' && " + "SYN_COUNT=$(ss -ant state syn-recv 2>/dev/null | tail -n +2 | wc -l) && " + "echo \"Current SYN_RECV connections: $SYN_COUNT\" && " + "echo '' && " + "if [ \"$SYN_COUNT\" -gt 50 ]; then " + " echo 'WARNING: Possible SYN flood detected! SYN_RECV count exceeds threshold (50)'; " + "elif [ \"$SYN_COUNT\" -gt 20 ]; then " + " echo 'NOTICE: Elevated SYN_RECV count. Monitor closely.'; " + "else " + " echo 'OK: SYN_RECV count is within normal range.'; " + "fi && " + "echo '' && " + "echo '--- Top Source IPs for SYN_RECV ---' && " + "ss -ant state syn-recv 2>/dev/null | awk 'NR>1 {split($4,a,\":\"); print a[1]}' | " + "sort | uniq -c | sort -rn | head -20 && " + "echo '' && " + "echo '--- SYN Cookies Status ---' && " + "SYNCOOKIE=$(sysctl -n net.ipv4.tcp_syncookies 2>/dev/null) && " + "if [ \"$SYNCOOKIE\" = \"1\" ]; then " + " echo 'SYN cookies: ENABLED (good)'; " + "else " + " echo 'SYN cookies: DISABLED (consider enabling)'; " + "fi" + ) + + +def bandwidth_stats_cmd(): + """Return bash cmd to show bandwidth usage.""" + return ( + "echo '=== Bandwidth Statistics ===' && " + "echo '' && " + "if command -v vnstat >/dev/null 2>&1; then " + " echo '--- vnstat Summary ---' && " + " vnstat 2>&1 && " + " echo '' && " + " echo '--- vnstat Live (5 second sample) ---' && " + " vnstat -tr 5 2>&1; " + "else " + " echo '--- /proc/net/dev (vnstat not installed) ---' && " + " echo 'Interface RX bytes TX bytes' && " + " awk 'NR>2 {split($0,a,\":\"); iface=a[1]; gsub(/^ +/,\"\",iface); " + " split(a[2],b,\" \"); printf \"%-16s %-16s %s\\n\", iface, b[1], b[9]}' " + " /proc/net/dev 2>/dev/null && " + " echo '' && " + " echo '--- Current Rate (2 second sample) ---' && " + " IFACE=$(ip route get 8.8.8.8 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i==\"dev\") print $(i+1); exit}') && " + " if [ -n \"$IFACE\" ]; then " + " RX1=$(cat /sys/class/net/$IFACE/statistics/rx_bytes 2>/dev/null) && " + " TX1=$(cat /sys/class/net/$IFACE/statistics/tx_bytes 2>/dev/null) && " + " sleep 2 && " + " RX2=$(cat /sys/class/net/$IFACE/statistics/rx_bytes 2>/dev/null) && " + " TX2=$(cat /sys/class/net/$IFACE/statistics/tx_bytes 2>/dev/null) && " + " RX_RATE=$(( (RX2 - RX1) / 2 )) && " + " TX_RATE=$(( (TX2 - TX1) / 2 )) && " + " echo \"Interface: $IFACE\" && " + " echo \"RX rate: $RX_RATE bytes/sec\" && " + " echo \"TX rate: $TX_RATE bytes/sec\"; " + " else " + " echo 'Could not determine primary interface'; " + " fi; " + "fi" + ) + + +def nginx_rate_limit_status_cmd(): + """Return bash cmd to check current nginx rate limiting config.""" + return ( + "echo '=== Nginx Rate Limiting Status ===' && " + "echo '' && " + "if ! command -v nginx >/dev/null 2>&1; then " + " echo 'nginx is not installed'; " + "else " + " echo '--- limit_req_zone directives ---' && " + " grep -rn 'limit_req_zone' /etc/nginx/ 2>/dev/null || echo 'None found' && " + " echo '' && " + " echo '--- limit_req directives ---' && " + " grep -rn 'limit_req ' /etc/nginx/ 2>/dev/null || echo 'None found' && " + " echo '' && " + " echo '--- limit_conn directives ---' && " + " grep -rn 'limit_conn' /etc/nginx/ 2>/dev/null || echo 'None found'; " + "fi" + ) + + +def nginx_rate_limit_cmd(requests_per_second=10, burst=20): + """Return bash cmd to configure nginx rate limiting.""" + zone_line = f"limit_req_zone $binary_remote_addr zone=setec_ratelimit:10m rate={requests_per_second}r/s;" + req_line = f"limit_req zone=setec_ratelimit burst={burst} nodelay;" + return ( + "echo '=== Configuring Nginx Rate Limiting ===' && " + "if ! command -v nginx >/dev/null 2>&1; then " + " echo 'ERROR: nginx is not installed'; exit 1; " + "fi && " + "cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%Y%m%d_%H%M%S) && " + "echo 'Backup created' && " + # Remove old setec rate limit config if present + "sed -i '/# setec-ratelimit-begin/,/# setec-ratelimit-end/d' /etc/nginx/nginx.conf && " + # Add zone in http block (after the http { line) + "sed -i '/^http {/a\\\\ # setec-ratelimit-begin\\n" + f" {zone_line}\\n" + " # setec-ratelimit-end' /etc/nginx/nginx.conf && " + # Add limit_req to each server block in sites-enabled + "for CONF in /etc/nginx/sites-enabled/*; do " + " [ -f \"$CONF\" ] || continue; " + " sed -i '/# setec-ratelimit-req-begin/,/# setec-ratelimit-req-end/d' \"$CONF\"; " + " sed -i '/^[[:space:]]*server {/a\\\\ # setec-ratelimit-req-begin\\n" + f" {req_line}\\n" + " limit_req_status 429;\\n" + " # setec-ratelimit-req-end' \"$CONF\"; " + "done && " + "nginx -t 2>&1 && " + "systemctl reload nginx 2>&1 && " + f"echo 'Rate limiting configured: {requests_per_second}r/s with burst={burst}'" + ) + + +def nginx_rate_limit_remove_cmd(): + """Return bash cmd to remove nginx rate limiting.""" + return ( + "echo '=== Removing Nginx Rate Limiting ===' && " + "if ! command -v nginx >/dev/null 2>&1; then " + " echo 'ERROR: nginx is not installed'; exit 1; " + "fi && " + "cp /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak.$(date +%Y%m%d_%H%M%S) && " + "sed -i '/# setec-ratelimit-begin/,/# setec-ratelimit-end/d' /etc/nginx/nginx.conf && " + "for CONF in /etc/nginx/sites-enabled/*; do " + " [ -f \"$CONF\" ] || continue; " + " sed -i '/# setec-ratelimit-req-begin/,/# setec-ratelimit-req-end/d' \"$CONF\"; " + "done && " + "nginx -t 2>&1 && " + "systemctl reload nginx 2>&1 && " + "echo 'Rate limiting removed and nginx reloaded'" + ) + + +def auto_blacklist_cmd(threshold=100, period=60): + """Return bash cmd to set up automatic IP blacklisting.""" + script = ( + "#!/bin/bash\\n" + "# Setec Labs Auto-Blacklist Script\\n" + "LOGFILE=/var/log/setec-blacklist.log\\n" + f"THRESHOLD={threshold}\\n" + "WHITELIST=\\\"127.0.0.1\\\"\\n" + "\\n" + "touch \\\"$LOGFILE\\\"\\n" + "\\n" + "# Get IPs with connections exceeding threshold\\n" + "ss -ant 2>/dev/null | awk 'NR>1 {split(\\$5,a,\\\":\\\"); print a[1]}' | \\\\\\n" + " grep -v '^$' | grep -v '\\\\*' | sort | uniq -c | sort -rn | \\\\\\n" + " while read COUNT IP; do\\n" + " # Skip whitelist\\n" + " echo \\\"$WHITELIST\\\" | grep -q \\\"$IP\\\" && continue\\n" + " # Skip private ranges\\n" + " echo \\\"$IP\\\" | grep -qE '^(10\\\\.|172\\\\.(1[6-9]|2[0-9]|3[01])\\\\.|192\\\\.168\\\\.)' && continue\\n" + " if [ \\\"$COUNT\\\" -gt \\\"$THRESHOLD\\\" ]; then\\n" + " # Check if already banned\\n" + " if ! iptables -C INPUT -s \\\"$IP\\\" -j DROP 2>/dev/null; then\\n" + " iptables -A INPUT -s \\\"$IP\\\" -j DROP\\n" + " echo \\\"$(date): Banned $IP ($COUNT connections)\\\" >> \\\"$LOGFILE\\\"\\n" + " fi\\n" + " fi\\n" + " done\\n" + ) + return ( + "echo '=== Setting Up Auto-Blacklisting ===' && " + f"echo -e '{script}' > /usr/local/bin/setec-blacklist.sh && " + "chmod +x /usr/local/bin/setec-blacklist.sh && " + "touch /var/log/setec-blacklist.log && " + "(crontab -l 2>/dev/null | grep -v 'setec-blacklist.sh'; " + "echo '* * * * * /usr/local/bin/setec-blacklist.sh') | crontab - && " + f"echo 'Auto-blacklist installed: threshold={threshold} connections' && " + "echo 'Cron job added: runs every minute'" + ) + + +def auto_blacklist_status_cmd(): + """Return bash cmd to check auto-blacklist status and recent bans.""" + return ( + "echo '=== Auto-Blacklist Status ===' && " + "echo '' && " + "echo '--- Script ---' && " + "if [ -f /usr/local/bin/setec-blacklist.sh ]; then " + " echo 'Script: INSTALLED'; " + " ls -la /usr/local/bin/setec-blacklist.sh; " + "else " + " echo 'Script: NOT INSTALLED'; " + "fi && " + "echo '' && " + "echo '--- Cron Job ---' && " + "if crontab -l 2>/dev/null | grep -q 'setec-blacklist.sh'; then " + " echo 'Cron: ACTIVE'; " + " crontab -l 2>/dev/null | grep 'setec-blacklist.sh'; " + "else " + " echo 'Cron: NOT FOUND'; " + "fi && " + "echo '' && " + "echo '--- Currently Banned IPs ---' && " + "iptables -L INPUT -n 2>/dev/null | grep DROP | awk '{print $4}' | sort | head -50 && " + "echo '' && " + "echo '--- Recent Ban Log (last 20) ---' && " + "if [ -f /var/log/setec-blacklist.log ]; then " + " tail -20 /var/log/setec-blacklist.log; " + "else " + " echo 'No ban log found'; " + "fi" + ) + + +def auto_blacklist_remove_cmd(): + """Return bash cmd to remove auto-blacklisting.""" + return ( + "echo '=== Removing Auto-Blacklisting ===' && " + "(crontab -l 2>/dev/null | grep -v 'setec-blacklist.sh') | crontab - && " + "rm -f /usr/local/bin/setec-blacklist.sh && " + "echo 'Auto-blacklist script removed' && " + "echo 'Cron job removed' && " + "echo 'Note: Existing iptables bans are still active. Use show_blacklist to review.'" + ) + + +def blacklist_ip_cmd(ip): + """Return bash cmd to manually blacklist an IP via iptables.""" + return ( + f"echo '=== Blacklisting IP: {ip} ===' && " + f"if iptables -C INPUT -s {ip} -j DROP 2>/dev/null; then " + f" echo 'IP {ip} is already blacklisted'; " + "else " + f" iptables -A INPUT -s {ip} -j DROP 2>&1 && " + f" echo 'IP {ip} has been blacklisted (DROP rule added)' && " + f" echo \"$(date): Manual ban {ip}\" >> /var/log/setec-blacklist.log; " + "fi" + ) + + +def unblacklist_ip_cmd(ip): + """Return bash cmd to remove an IP from blacklist.""" + return ( + f"echo '=== Removing IP from Blacklist: {ip} ===' && " + f"if iptables -C INPUT -s {ip} -j DROP 2>/dev/null; then " + f" iptables -D INPUT -s {ip} -j DROP 2>&1 && " + f" echo 'IP {ip} has been removed from blacklist' && " + f" echo \"$(date): Manual unban {ip}\" >> /var/log/setec-blacklist.log; " + "else " + f" echo 'IP {ip} was not found in blacklist'; " + "fi" + ) + + +def show_blacklist_cmd(): + """Return bash cmd to show all blacklisted IPs.""" + return ( + "echo '=== Current IP Blacklist ===' && " + "echo '' && " + "iptables -L INPUT -n --line-numbers 2>/dev/null | grep DROP && " + "echo '' && " + "COUNT=$(iptables -L INPUT -n 2>/dev/null | grep -c DROP) && " + "echo \"Total blacklisted IPs: $COUNT\"" + ) + + +def syn_protection_cmd(): + """Return bash cmd to enable SYN flood protection via sysctl and iptables.""" + return ( + "echo '=== Enabling SYN Flood Protection ===' && " + "echo '' && " + "echo '--- Sysctl Tuning ---' && " + "sysctl -w net.ipv4.tcp_syncookies=1 2>&1 && " + "sysctl -w net.ipv4.tcp_max_syn_backlog=2048 2>&1 && " + "sysctl -w net.ipv4.tcp_synack_retries=2 2>&1 && " + "sysctl -w net.ipv4.tcp_syn_retries=2 2>&1 && " + "sysctl -w net.ipv4.conf.all.rp_filter=1 2>&1 && " + "sysctl -w net.ipv4.tcp_timestamps=1 2>&1 && " + # Persist settings + "cat >> /etc/sysctl.d/99-syn-protection.conf << 'SYSCTL_EOF'\n" + "# Setec SYN flood protection\n" + "net.ipv4.tcp_syncookies = 1\n" + "net.ipv4.tcp_max_syn_backlog = 2048\n" + "net.ipv4.tcp_synack_retries = 2\n" + "net.ipv4.tcp_syn_retries = 2\n" + "net.ipv4.conf.all.rp_filter = 1\n" + "net.ipv4.tcp_timestamps = 1\n" + "SYSCTL_EOF\n" + " && " + "echo '' && " + "echo '--- iptables SYN Rate Limiting ---' && " + # Remove old rules if they exist + "iptables -D INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j ACCEPT 2>/dev/null; " + "iptables -D INPUT -p tcp --syn -j DROP 2>/dev/null; " + # Add new rules + "iptables -A INPUT -p tcp --syn -m limit --limit 1/s --limit-burst 3 -j ACCEPT 2>&1 && " + "echo 'SYN rate limit: 1/s with burst of 3' && " + "echo '' && " + "echo 'SYN flood protection enabled' && " + "echo 'Sysctl settings persisted to /etc/sysctl.d/99-syn-protection.conf'" + ) + + +def cloudflare_under_attack_cmd(zone_id, api_token, enable=True): + """Return bash cmd to toggle Cloudflare Under Attack mode.""" + level = "under_attack" if enable else "medium" + action = "Enabling Under Attack mode" if enable else "Disabling Under Attack mode (setting to medium)" + return ( + f"echo '=== Cloudflare: {action} ===' && " + "if ! command -v curl >/dev/null 2>&1; then " + " echo 'ERROR: curl is required'; exit 1; " + "fi && " + f"RESPONSE=$(curl -s -X PATCH " + f"'https://api.cloudflare.com/client/v4/zones/{zone_id}/settings/security_level' " + f"-H 'Authorization: Bearer {api_token}' " + f"-H 'Content-Type: application/json' " + f"--data '{{\"value\":\"{level}\"}}' 2>&1) && " + "echo \"$RESPONSE\" | " + "if command -v jq >/dev/null 2>&1; then " + " jq -r '\"Success: \" + (.success|tostring) + \" | Level: \" + .result.value' 2>/dev/null || echo \"$RESPONSE\"; " + "else " + " grep -o '\"success\":[a-z]*' 2>/dev/null || echo \"$RESPONSE\"; " + "fi" + ) + + +def cloudflare_status_cmd(zone_id, api_token): + """Return bash cmd to check current Cloudflare security level.""" + return ( + "echo '=== Cloudflare Security Status ===' && " + "if ! command -v curl >/dev/null 2>&1; then " + " echo 'ERROR: curl is required'; exit 1; " + "fi && " + f"RESPONSE=$(curl -s -X GET " + f"'https://api.cloudflare.com/client/v4/zones/{zone_id}/settings/security_level' " + f"-H 'Authorization: Bearer {api_token}' " + f"-H 'Content-Type: application/json' 2>&1) && " + "echo \"$RESPONSE\" | " + "if command -v jq >/dev/null 2>&1; then " + " jq -r '\"Security Level: \" + .result.value + \" | Modified: \" + .result.modified_on' 2>/dev/null || echo \"$RESPONSE\"; " + "else " + " grep -oP '\"value\":\"[^\"]+\"' 2>/dev/null || echo \"$RESPONSE\"; " + "fi" + ) + + +def tor_exit_block_cmd(enable=True): + """Return bash cmd to download and block Tor exit nodes.""" + if enable: + return ( + "echo '=== Blocking Tor Exit Nodes ===' && " + "if ! command -v curl >/dev/null 2>&1 && ! command -v wget >/dev/null 2>&1; then " + " echo 'ERROR: curl or wget required'; exit 1; " + "fi && " + # Check for ipset + "if ! command -v ipset >/dev/null 2>&1; then " + " echo 'Installing ipset...' && " + " apt-get install -y ipset 2>&1 || yum install -y ipset 2>&1; " + "fi && " + # Create or flush ipset + "ipset create tor_exit hash:ip -exist 2>&1 && " + "ipset flush tor_exit 2>&1 && " + # Download Tor exit node list + "echo 'Downloading Tor exit node list...' && " + "TOR_LIST=$(mktemp) && " + "if command -v curl >/dev/null 2>&1; then " + " curl -s 'https://check.torproject.org/torbulkexitlist' > \"$TOR_LIST\" 2>&1; " + "else " + " wget -q 'https://check.torproject.org/torbulkexitlist' -O \"$TOR_LIST\" 2>&1; " + "fi && " + "COUNT=0 && " + "while IFS= read -r IP; do " + " echo \"$IP\" | grep -qE '^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$' || continue; " + " ipset add tor_exit \"$IP\" -exist 2>/dev/null; " + " COUNT=$((COUNT + 1)); " + "done < \"$TOR_LIST\" && " + "rm -f \"$TOR_LIST\" && " + # Add iptables rule if not present + "if ! iptables -C INPUT -m set --match-set tor_exit src -j DROP 2>/dev/null; then " + " iptables -A INPUT -m set --match-set tor_exit src -j DROP 2>&1; " + "fi && " + "echo \"Blocked $COUNT Tor exit nodes\" && " + # Save for persistence + "ipset save > /etc/ipset.conf 2>/dev/null; " + "echo 'Tor exit node blocking enabled'" + ) + else: + return ( + "echo '=== Removing Tor Exit Node Blocking ===' && " + "iptables -D INPUT -m set --match-set tor_exit src -j DROP 2>/dev/null; " + "ipset destroy tor_exit 2>/dev/null; " + "echo 'Tor exit node blocking removed'" + ) + + +def geoblock_cmd(country_codes): + """Return bash cmd to block traffic from specific countries using xtables-addons/geoip.""" + if isinstance(country_codes, str): + codes = country_codes + else: + codes = ",".join(country_codes) + + return ( + f"echo '=== GeoIP Blocking: {codes} ===' && " + "echo '' && " + # Check/install xtables-addons + "if ! modprobe xt_geoip 2>/dev/null; then " + " echo 'Installing xtables-addons...' && " + " apt-get install -y xtables-addons-common libtext-csv-xs-perl 2>&1 || " + " yum install -y xtables-addons 2>&1; " + "fi && " + # Download GeoIP database + "echo 'Updating GeoIP database...' && " + "mkdir -p /usr/share/xt_geoip && " + "if [ -d /usr/lib/xtables-addons ] || [ -d /usr/libexec/xtables-addons ]; then " + " DBDIR=$(find /usr/lib /usr/libexec -name 'xt_geoip_dl' -type f 2>/dev/null | head -1 | xargs dirname 2>/dev/null) && " + " if [ -n \"$DBDIR\" ]; then " + " cd /tmp && " + " \"$DBDIR/xt_geoip_dl\" 2>&1 && " + " \"$DBDIR/xt_geoip_build\" -D /usr/share/xt_geoip *.csv 2>&1; " + " else " + " echo 'WARNING: Could not find xt_geoip_dl. GeoIP DB may be outdated.'; " + " fi; " + "else " + " echo 'WARNING: xtables-addons path not found'; " + "fi && " + "echo '' && " + # Remove old setec geoblock rules + "iptables-save 2>/dev/null | grep 'setec-geoblock' | while read RULE; do " + " CLEAN=$(echo \"$RULE\" | sed 's/^-A //' ); " + " iptables -D $CLEAN 2>/dev/null; " + "done; " + # Add rules for each country + f"for CC in $(echo '{codes}' | tr ',' ' '); do " + " iptables -A INPUT -m geoip --src-cc \"$CC\" -j DROP " + " -m comment --comment 'setec-geoblock' 2>&1 && " + " echo \"Blocked country: $CC\"; " + "done && " + "echo '' && " + f"echo 'GeoIP blocking active for: {codes}'" + ) diff --git a/setec-web/detector.py b/setec-web/detector.py new file mode 100644 index 0000000..bf9ba0d --- /dev/null +++ b/setec-web/detector.py @@ -0,0 +1,437 @@ +# Service/daemon detection heuristics +# Detects installed server software by checking processes, ports, packages, and config files + +SERVICES = [ + # ── Web Servers ── + {"name": "Nginx", "cat": "web", "process": ["nginx"], "ports": [80, 443, 8080], "pkg": ["nginx", "nginx-full", "nginx-extras"], "configs": ["/etc/nginx/nginx.conf", "/etc/nginx/sites-available/", "/etc/nginx/conf.d/"]}, + {"name": "Apache", "cat": "web", "process": ["apache2", "httpd"], "ports": [80, 443, 8080], "pkg": ["apache2", "httpd"], "configs": ["/etc/apache2/apache2.conf", "/etc/httpd/conf/httpd.conf", "/etc/apache2/sites-available/"]}, + {"name": "Caddy", "cat": "web", "process": ["caddy"], "ports": [80, 443], "pkg": ["caddy"], "configs": ["/etc/caddy/Caddyfile"]}, + {"name": "Lighttpd", "cat": "web", "process": ["lighttpd"], "ports": [80], "pkg": ["lighttpd"], "configs": ["/etc/lighttpd/lighttpd.conf"]}, + {"name": "HAProxy", "cat": "web", "process": ["haproxy"], "ports": [80, 443, 8404], "pkg": ["haproxy"], "configs": ["/etc/haproxy/haproxy.cfg"]}, + {"name": "Traefik", "cat": "web", "process": ["traefik"], "ports": [80, 443, 8080], "pkg": [], "configs": ["/etc/traefik/traefik.yml", "/etc/traefik/traefik.toml"]}, + {"name": "Varnish", "cat": "web", "process": ["varnishd"], "ports": [6081, 6082], "pkg": ["varnish"], "configs": ["/etc/varnish/default.vcl"]}, + {"name": "Squid", "cat": "web", "process": ["squid"], "ports": [3128], "pkg": ["squid"], "configs": ["/etc/squid/squid.conf"]}, + + # ── SSH / Remote Access ── + {"name": "OpenSSH", "cat": "remote", "process": ["sshd"], "ports": [22, 2222], "pkg": ["openssh-server"], "configs": ["/etc/ssh/sshd_config", "/etc/ssh/ssh_config"]}, + {"name": "Dropbear", "cat": "remote", "process": ["dropbear"], "ports": [22], "pkg": ["dropbear"], "configs": ["/etc/dropbear/", "/etc/default/dropbear"]}, + {"name": "Mosh", "cat": "remote", "process": ["mosh-server"], "ports": [], "pkg": ["mosh"], "configs": []}, + {"name": "x2go", "cat": "remote", "process": ["x2goserver"], "ports": [22], "pkg": ["x2goserver"], "configs": ["/etc/x2go/"]}, + {"name": "xrdp", "cat": "remote", "process": ["xrdp"], "ports": [3389], "pkg": ["xrdp"], "configs": ["/etc/xrdp/xrdp.ini", "/etc/xrdp/sesman.ini"]}, + {"name": "VNC (TigerVNC)", "cat": "remote", "process": ["Xvnc", "x0vncserver"], "ports": [5900, 5901], "pkg": ["tigervnc-standalone-server"], "configs": ["~/.vnc/config"]}, + {"name": "Cockpit", "cat": "remote", "process": ["cockpit-ws"], "ports": [9090], "pkg": ["cockpit"], "configs": ["/etc/cockpit/cockpit.conf"]}, + {"name": "Webmin", "cat": "remote", "process": ["miniserv.pl"], "ports": [10000], "pkg": ["webmin"], "configs": ["/etc/webmin/miniserv.conf"]}, + {"name": "Shellinabox", "cat": "remote", "process": ["shellinaboxd"], "ports": [4200], "pkg": ["shellinabox"], "configs": ["/etc/default/shellinabox"]}, + + # ── FTP / File Transfer ── + {"name": "vsftpd", "cat": "ftp", "process": ["vsftpd"], "ports": [21], "pkg": ["vsftpd"], "configs": ["/etc/vsftpd.conf", "/etc/vsftpd/vsftpd.conf"]}, + {"name": "ProFTPD", "cat": "ftp", "process": ["proftpd"], "ports": [21], "pkg": ["proftpd", "proftpd-basic"], "configs": ["/etc/proftpd/proftpd.conf"]}, + {"name": "Pure-FTPd", "cat": "ftp", "process": ["pure-ftpd"], "ports": [21], "pkg": ["pure-ftpd"], "configs": ["/etc/pure-ftpd/pure-ftpd.conf", "/etc/pure-ftpd/conf/"]}, + {"name": "SFTP (OpenSSH)", "cat": "ftp", "process": ["sftp-server"], "ports": [22], "pkg": [], "configs": ["/etc/ssh/sshd_config"]}, + {"name": "rsyncd", "cat": "ftp", "process": ["rsync"], "ports": [873], "pkg": ["rsync"], "configs": ["/etc/rsyncd.conf"]}, + + # ── DNS ── + {"name": "BIND9", "cat": "dns", "process": ["named"], "ports": [53], "pkg": ["bind9"], "configs": ["/etc/bind/named.conf", "/etc/bind/named.conf.local", "/etc/bind/named.conf.options"]}, + {"name": "Unbound", "cat": "dns", "process": ["unbound"], "ports": [53], "pkg": ["unbound"], "configs": ["/etc/unbound/unbound.conf"]}, + {"name": "dnsmasq", "cat": "dns", "process": ["dnsmasq"], "ports": [53], "pkg": ["dnsmasq"], "configs": ["/etc/dnsmasq.conf", "/etc/dnsmasq.d/"]}, + {"name": "PowerDNS", "cat": "dns", "process": ["pdns_server"], "ports": [53], "pkg": ["pdns-server"], "configs": ["/etc/powerdns/pdns.conf"]}, + {"name": "CoreDNS", "cat": "dns", "process": ["coredns"], "ports": [53], "pkg": [], "configs": ["/etc/coredns/Corefile"]}, + {"name": "Pi-hole", "cat": "dns", "process": ["pihole-FTL"], "ports": [53, 80], "pkg": ["pihole"], "configs": ["/etc/pihole/setupVars.conf", "/etc/pihole/pihole-FTL.conf"]}, + {"name": "AdGuard Home", "cat": "dns", "process": ["AdGuardHome"], "ports": [53, 3000], "pkg": [], "configs": ["/opt/AdGuardHome/AdGuardHome.yaml"]}, + + # ── Mail ── + {"name": "Postfix", "cat": "mail", "process": ["master"], "ports": [25, 587], "pkg": ["postfix"], "configs": ["/etc/postfix/main.cf", "/etc/postfix/master.cf"]}, + {"name": "Exim", "cat": "mail", "process": ["exim4", "exim"], "ports": [25, 587], "pkg": ["exim4", "exim4-daemon-heavy"], "configs": ["/etc/exim4/exim4.conf.template", "/etc/exim4/update-exim4.conf.conf"]}, + {"name": "Sendmail", "cat": "mail", "process": ["sendmail"], "ports": [25], "pkg": ["sendmail"], "configs": ["/etc/mail/sendmail.cf", "/etc/mail/sendmail.mc"]}, + {"name": "Dovecot", "cat": "mail", "process": ["dovecot"], "ports": [143, 993, 110, 995], "pkg": ["dovecot-core", "dovecot-imapd"], "configs": ["/etc/dovecot/dovecot.conf", "/etc/dovecot/conf.d/"]}, + {"name": "OpenDKIM", "cat": "mail", "process": ["opendkim"], "ports": [8891], "pkg": ["opendkim"], "configs": ["/etc/opendkim.conf", "/etc/opendkim/"]}, + {"name": "OpenDMARC", "cat": "mail", "process": ["opendmarc"], "ports": [8893], "pkg": ["opendmarc"], "configs": ["/etc/opendmarc.conf"]}, + {"name": "SpamAssassin", "cat": "mail", "process": ["spamd"], "ports": [783], "pkg": ["spamassassin"], "configs": ["/etc/spamassassin/local.cf"]}, + {"name": "ClamAV", "cat": "mail", "process": ["clamd", "freshclam"], "ports": [3310], "pkg": ["clamav", "clamav-daemon"], "configs": ["/etc/clamav/clamd.conf", "/etc/clamav/freshclam.conf"]}, + {"name": "Amavis", "cat": "mail", "process": ["amavisd", "amavisd-new"], "ports": [10024], "pkg": ["amavisd-new"], "configs": ["/etc/amavis/conf.d/"]}, + {"name": "Roundcube", "cat": "mail", "process": [], "ports": [], "pkg": ["roundcube"], "configs": ["/etc/roundcube/config.inc.php", "/var/www/roundcube/config/config.inc.php"]}, + {"name": "Rainloop", "cat": "mail", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/rainloop/data/_data_/_default_/configs/application.ini"]}, + + # ── Databases ── + {"name": "MySQL/MariaDB", "cat": "db", "process": ["mysqld", "mariadbd"], "ports": [3306], "pkg": ["mysql-server", "mariadb-server"], "configs": ["/etc/mysql/my.cnf", "/etc/mysql/mariadb.conf.d/", "/etc/mysql/mysql.conf.d/", "/etc/my.cnf"]}, + {"name": "PostgreSQL", "cat": "db", "process": ["postgres", "postgresql"], "ports": [5432], "pkg": ["postgresql"], "configs": ["/etc/postgresql/", "/var/lib/postgresql/data/postgresql.conf"]}, + {"name": "MongoDB", "cat": "db", "process": ["mongod", "mongos"], "ports": [27017], "pkg": ["mongodb-org", "mongod"], "configs": ["/etc/mongod.conf"]}, + {"name": "Redis", "cat": "db", "process": ["redis-server"], "ports": [6379], "pkg": ["redis-server", "redis"], "configs": ["/etc/redis/redis.conf", "/etc/redis.conf"]}, + {"name": "Memcached", "cat": "db", "process": ["memcached"], "ports": [11211], "pkg": ["memcached"], "configs": ["/etc/memcached.conf"]}, + {"name": "SQLite", "cat": "db", "process": [], "ports": [], "pkg": ["sqlite3"], "configs": []}, + {"name": "CouchDB", "cat": "db", "process": ["beam.smp"], "ports": [5984], "pkg": ["couchdb"], "configs": ["/etc/couchdb/local.ini"]}, + {"name": "InfluxDB", "cat": "db", "process": ["influxd"], "ports": [8086], "pkg": ["influxdb", "influxdb2"], "configs": ["/etc/influxdb/influxdb.conf", "/etc/influxdb/config.toml"]}, + {"name": "Elasticsearch", "cat": "db", "process": ["java.*elasticsearch"], "ports": [9200, 9300], "pkg": ["elasticsearch"], "configs": ["/etc/elasticsearch/elasticsearch.yml"]}, + {"name": "Cassandra", "cat": "db", "process": ["java.*cassandra"], "ports": [9042, 7000], "pkg": ["cassandra"], "configs": ["/etc/cassandra/cassandra.yaml"]}, + {"name": "Neo4j", "cat": "db", "process": ["java.*neo4j"], "ports": [7474, 7687], "pkg": ["neo4j"], "configs": ["/etc/neo4j/neo4j.conf"]}, + {"name": "RethinkDB", "cat": "db", "process": ["rethinkdb"], "ports": [8080, 28015], "pkg": ["rethinkdb"], "configs": ["/etc/rethinkdb/instances.d/"]}, + {"name": "ClickHouse", "cat": "db", "process": ["clickhouse-server"], "ports": [8123, 9000], "pkg": ["clickhouse-server"], "configs": ["/etc/clickhouse-server/config.xml"]}, + {"name": "etcd", "cat": "db", "process": ["etcd"], "ports": [2379, 2380], "pkg": ["etcd"], "configs": ["/etc/etcd/etcd.conf.yml"]}, + {"name": "Valkey", "cat": "db", "process": ["valkey-server"], "ports": [6379], "pkg": ["valkey"], "configs": ["/etc/valkey/valkey.conf"]}, + {"name": "KeyDB", "cat": "db", "process": ["keydb-server"], "ports": [6379], "pkg": ["keydb"], "configs": ["/etc/keydb/keydb.conf"]}, + + # ── DB Admin Tools ── + {"name": "phpMyAdmin", "cat": "dbadmin", "process": [], "ports": [], "pkg": ["phpmyadmin"], "configs": ["/etc/phpmyadmin/config.inc.php"]}, + {"name": "Adminer", "cat": "dbadmin", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/adminer/"]}, + {"name": "pgAdmin", "cat": "dbadmin", "process": ["pgadmin"], "ports": [5050], "pkg": ["pgadmin4"], "configs": ["/etc/pgadmin/config_system.py"]}, + + # ── Git / Code Forges ── + {"name": "Gitea", "cat": "git", "process": ["gitea"], "ports": [3000], "pkg": [], "configs": ["/etc/gitea/app.ini", "/var/lib/gitea/custom/conf/app.ini"]}, + {"name": "GitLab", "cat": "git", "process": ["gitlab-workhorse", "puma", "sidekiq", "gitaly"], "ports": [8080, 80, 443], "pkg": ["gitlab-ce", "gitlab-ee"], "configs": ["/etc/gitlab/gitlab.rb"]}, + {"name": "Gogs", "cat": "git", "process": ["gogs"], "ports": [3000], "pkg": [], "configs": ["/etc/gogs/app.ini", "/home/git/gogs/custom/conf/app.ini"]}, + {"name": "Forgejo", "cat": "git", "process": ["forgejo"], "ports": [3000], "pkg": [], "configs": ["/etc/forgejo/app.ini"]}, + {"name": "cgit", "cat": "git", "process": [], "ports": [], "pkg": ["cgit"], "configs": ["/etc/cgitrc"]}, + {"name": "Gitolite", "cat": "git", "process": [], "ports": [], "pkg": ["gitolite3"], "configs": ["~/.gitolite.rc"]}, + {"name": "GitBucket", "cat": "git", "process": ["java.*gitbucket"], "ports": [8080], "pkg": [], "configs": ["~/.gitbucket/gitbucket.conf"]}, + {"name": "OneDev", "cat": "git", "process": ["java.*onedev"], "ports": [6610, 6611], "pkg": [], "configs": ["/opt/onedev/conf/server.properties"]}, + + # ── CI/CD ── + {"name": "Jenkins", "cat": "ci", "process": ["java.*jenkins"], "ports": [8080], "pkg": ["jenkins"], "configs": ["/etc/default/jenkins", "/var/lib/jenkins/config.xml"]}, + {"name": "Drone", "cat": "ci", "process": ["drone-server"], "ports": [80, 443], "pkg": [], "configs": []}, + {"name": "Woodpecker CI", "cat": "ci", "process": ["woodpecker-server"], "ports": [8000], "pkg": [], "configs": []}, + {"name": "GoCD", "cat": "ci", "process": ["java.*go.jar"], "ports": [8153, 8154], "pkg": ["go-server"], "configs": ["/etc/go/cruise-config.xml"]}, + {"name": "Buildbot", "cat": "ci", "process": ["buildbot"], "ports": [8010], "pkg": ["buildbot"], "configs": ["/etc/buildbot/"]}, + {"name": "Concourse", "cat": "ci", "process": ["concourse"], "ports": [8080], "pkg": [], "configs": []}, + {"name": "GitLab Runner", "cat": "ci", "process": ["gitlab-runner"], "ports": [], "pkg": ["gitlab-runner"], "configs": ["/etc/gitlab-runner/config.toml"]}, + {"name": "Act Runner", "cat": "ci", "process": ["act_runner"], "ports": [], "pkg": [], "configs": ["/etc/act_runner/config.yaml"]}, + + # ── Containers / Orchestration ── + {"name": "Docker", "cat": "container", "process": ["dockerd", "containerd"], "ports": [2375, 2376], "pkg": ["docker-ce", "docker.io"], "configs": ["/etc/docker/daemon.json"]}, + {"name": "Podman", "cat": "container", "process": ["podman"], "ports": [], "pkg": ["podman"], "configs": ["/etc/containers/registries.conf", "/etc/containers/policy.json"]}, + {"name": "LXD/LXC", "cat": "container", "process": ["lxd", "lxcfs"], "ports": [8443], "pkg": ["lxd", "lxc"], "configs": ["/etc/lxc/default.conf"]}, + {"name": "Portainer", "cat": "container", "process": ["portainer"], "ports": [9000, 9443], "pkg": [], "configs": []}, + {"name": "Kubernetes (k3s)", "cat": "container", "process": ["k3s"], "ports": [6443], "pkg": [], "configs": ["/etc/rancher/k3s/config.yaml"]}, + {"name": "MicroK8s", "cat": "container", "process": ["microk8s"], "ports": [16443], "pkg": ["microk8s"], "configs": []}, + {"name": "Nomad", "cat": "container", "process": ["nomad"], "ports": [4646], "pkg": ["nomad"], "configs": ["/etc/nomad.d/"]}, + {"name": "Docker Registry", "cat": "container", "process": ["registry"], "ports": [5000], "pkg": [], "configs": ["/etc/docker/registry/config.yml"]}, + {"name": "Yacht", "cat": "container", "process": [], "ports": [8000], "pkg": [], "configs": []}, + + # ── Monitoring ── + {"name": "Prometheus", "cat": "monitor", "process": ["prometheus"], "ports": [9090], "pkg": ["prometheus"], "configs": ["/etc/prometheus/prometheus.yml"]}, + {"name": "Grafana", "cat": "monitor", "process": ["grafana-server", "grafana"], "ports": [3000], "pkg": ["grafana"], "configs": ["/etc/grafana/grafana.ini"]}, + {"name": "Zabbix", "cat": "monitor", "process": ["zabbix_server", "zabbix_agentd"], "ports": [10051, 10050], "pkg": ["zabbix-server-mysql", "zabbix-agent"], "configs": ["/etc/zabbix/zabbix_server.conf", "/etc/zabbix/zabbix_agentd.conf"]}, + {"name": "Nagios", "cat": "monitor", "process": ["nagios"], "ports": [80], "pkg": ["nagios4"], "configs": ["/etc/nagios4/nagios.cfg", "/usr/local/nagios/etc/nagios.cfg"]}, + {"name": "Netdata", "cat": "monitor", "process": ["netdata"], "ports": [19999], "pkg": ["netdata"], "configs": ["/etc/netdata/netdata.conf"]}, + {"name": "Node Exporter", "cat": "monitor", "process": ["node_exporter"], "ports": [9100], "pkg": ["prometheus-node-exporter"], "configs": []}, + {"name": "Telegraf", "cat": "monitor", "process": ["telegraf"], "ports": [], "pkg": ["telegraf"], "configs": ["/etc/telegraf/telegraf.conf"]}, + {"name": "Collectd", "cat": "monitor", "process": ["collectd"], "ports": [25826], "pkg": ["collectd"], "configs": ["/etc/collectd/collectd.conf"]}, + {"name": "Icinga2", "cat": "monitor", "process": ["icinga2"], "ports": [5665], "pkg": ["icinga2"], "configs": ["/etc/icinga2/icinga2.conf"]}, + {"name": "Uptime Kuma", "cat": "monitor", "process": ["uptime-kuma"], "ports": [3001], "pkg": [], "configs": []}, + {"name": "Monit", "cat": "monitor", "process": ["monit"], "ports": [2812], "pkg": ["monit"], "configs": ["/etc/monit/monitrc", "/etc/monitrc"]}, + {"name": "Checkmk", "cat": "monitor", "process": ["cmk"], "ports": [80], "pkg": ["check-mk-raw"], "configs": ["/etc/check_mk/main.mk"]}, + {"name": "Loki", "cat": "monitor", "process": ["loki"], "ports": [3100], "pkg": [], "configs": ["/etc/loki/config.yaml"]}, + {"name": "Promtail", "cat": "monitor", "process": ["promtail"], "ports": [9080], "pkg": [], "configs": ["/etc/promtail/config.yml"]}, + {"name": "Graylog", "cat": "monitor", "process": ["java.*graylog"], "ports": [9000, 12201], "pkg": ["graylog-server"], "configs": ["/etc/graylog/server/server.conf"]}, + {"name": "Fluentd", "cat": "monitor", "process": ["fluentd", "td-agent"], "ports": [24224], "pkg": ["td-agent"], "configs": ["/etc/td-agent/td-agent.conf", "/etc/fluent/fluent.conf"]}, + + # ── VPN / Tunnels ── + {"name": "WireGuard", "cat": "vpn", "process": [], "ports": [51820], "pkg": ["wireguard", "wireguard-tools"], "configs": ["/etc/wireguard/wg0.conf"]}, + {"name": "OpenVPN", "cat": "vpn", "process": ["openvpn"], "ports": [1194], "pkg": ["openvpn"], "configs": ["/etc/openvpn/server.conf", "/etc/openvpn/server/"]}, + {"name": "StrongSwan (IPsec)", "cat": "vpn", "process": ["charon", "starter"], "ports": [500, 4500], "pkg": ["strongswan"], "configs": ["/etc/ipsec.conf", "/etc/strongswan.conf"]}, + {"name": "Tailscale", "cat": "vpn", "process": ["tailscaled"], "ports": [], "pkg": ["tailscale"], "configs": []}, + {"name": "ZeroTier", "cat": "vpn", "process": ["zerotier-one"], "ports": [9993], "pkg": ["zerotier-one"], "configs": ["/var/lib/zerotier-one/"]}, + {"name": "Headscale", "cat": "vpn", "process": ["headscale"], "ports": [8080], "pkg": [], "configs": ["/etc/headscale/config.yaml"]}, + {"name": "Nebula", "cat": "vpn", "process": ["nebula"], "ports": [4242], "pkg": [], "configs": ["/etc/nebula/config.yml"]}, + {"name": "frp", "cat": "vpn", "process": ["frps", "frpc"], "ports": [7000, 7500], "pkg": [], "configs": ["/etc/frp/frps.ini", "/etc/frp/frps.toml"]}, + {"name": "Cloudflared", "cat": "vpn", "process": ["cloudflared"], "ports": [], "pkg": ["cloudflared"], "configs": ["/etc/cloudflared/config.yml"]}, + {"name": "ngrok", "cat": "vpn", "process": ["ngrok"], "ports": [4040], "pkg": [], "configs": ["~/.ngrok2/ngrok.yml"]}, + + # ── Firewall / Security ── + {"name": "UFW", "cat": "security", "process": [], "ports": [], "pkg": ["ufw"], "configs": ["/etc/ufw/ufw.conf", "/etc/ufw/user.rules", "/etc/ufw/user6.rules"]}, + {"name": "iptables", "cat": "security", "process": [], "ports": [], "pkg": ["iptables"], "configs": ["/etc/iptables/rules.v4", "/etc/iptables/rules.v6"]}, + {"name": "nftables", "cat": "security", "process": ["nft"], "ports": [], "pkg": ["nftables"], "configs": ["/etc/nftables.conf"]}, + {"name": "Fail2Ban", "cat": "security", "process": ["fail2ban-server"], "ports": [], "pkg": ["fail2ban"], "configs": ["/etc/fail2ban/jail.conf", "/etc/fail2ban/jail.local", "/etc/fail2ban/jail.d/"]}, + {"name": "CrowdSec", "cat": "security", "process": ["crowdsec"], "ports": [8080], "pkg": ["crowdsec"], "configs": ["/etc/crowdsec/config.yaml"]}, + {"name": "OSSEC", "cat": "security", "process": ["ossec-analysisd"], "ports": [1514], "pkg": ["ossec-hids"], "configs": ["/var/ossec/etc/ossec.conf"]}, + {"name": "Snort", "cat": "security", "process": ["snort"], "ports": [], "pkg": ["snort"], "configs": ["/etc/snort/snort.conf"]}, + {"name": "Suricata", "cat": "security", "process": ["suricata"], "ports": [], "pkg": ["suricata"], "configs": ["/etc/suricata/suricata.yaml"]}, + {"name": "ModSecurity", "cat": "security", "process": [], "ports": [], "pkg": ["libapache2-mod-security2"], "configs": ["/etc/modsecurity/modsecurity.conf"]}, + {"name": "AppArmor", "cat": "security", "process": ["apparmor"], "ports": [], "pkg": ["apparmor"], "configs": ["/etc/apparmor.d/"]}, + {"name": "SELinux", "cat": "security", "process": [], "ports": [], "pkg": ["selinux-utils"], "configs": ["/etc/selinux/config"]}, + {"name": "Wazuh", "cat": "security", "process": ["wazuh-manager"], "ports": [1514, 1515, 55000], "pkg": ["wazuh-manager"], "configs": ["/var/ossec/etc/ossec.conf"]}, + + # ── Auth / SSO ── + {"name": "Keycloak", "cat": "auth", "process": ["java.*keycloak"], "ports": [8080, 8443], "pkg": [], "configs": ["/opt/keycloak/conf/keycloak.conf"]}, + {"name": "Authentik", "cat": "auth", "process": ["authentik"], "ports": [9000, 9443], "pkg": [], "configs": []}, + {"name": "Authelia", "cat": "auth", "process": ["authelia"], "ports": [9091], "pkg": [], "configs": ["/etc/authelia/configuration.yml"]}, + {"name": "OpenLDAP", "cat": "auth", "process": ["slapd"], "ports": [389, 636], "pkg": ["slapd", "ldap-utils"], "configs": ["/etc/ldap/slapd.d/", "/etc/openldap/slapd.conf"]}, + {"name": "FreeIPA", "cat": "auth", "process": ["ipa"], "ports": [80, 443, 389, 636], "pkg": ["freeipa-server"], "configs": ["/etc/ipa/default.conf"]}, + {"name": "Vault", "cat": "auth", "process": ["vault"], "ports": [8200], "pkg": ["vault"], "configs": ["/etc/vault.d/vault.hcl"]}, + {"name": "Teleport", "cat": "auth", "process": ["teleport"], "ports": [3023, 3024, 3080], "pkg": ["teleport"], "configs": ["/etc/teleport.yaml"]}, + + # ── CMS / Web Apps ── + {"name": "WordPress", "cat": "cms", "process": ["php-fpm"], "ports": [], "pkg": ["wordpress"], "configs": ["/var/www/html/wp-config.php", "/var/www/wordpress/wp-config.php"]}, + {"name": "Ghost", "cat": "cms", "process": ["ghost"], "ports": [2368], "pkg": [], "configs": ["/var/www/ghost/config.production.json"]}, + {"name": "Drupal", "cat": "cms", "process": [], "ports": [], "pkg": ["drupal"], "configs": ["/var/www/html/sites/default/settings.php"]}, + {"name": "Joomla", "cat": "cms", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/html/configuration.php"]}, + {"name": "MediaWiki", "cat": "cms", "process": [], "ports": [], "pkg": ["mediawiki"], "configs": ["/var/www/html/LocalSettings.php", "/etc/mediawiki/LocalSettings.php"]}, + {"name": "Wiki.js", "cat": "cms", "process": ["wiki"], "ports": [3000], "pkg": [], "configs": ["/etc/wiki/config.yml"]}, + {"name": "BookStack", "cat": "cms", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/bookstack/.env"]}, + {"name": "DokuWiki", "cat": "cms", "process": [], "ports": [], "pkg": ["dokuwiki"], "configs": ["/var/www/dokuwiki/conf/local.php"]}, + {"name": "Hugo", "cat": "cms", "process": ["hugo"], "ports": [1313], "pkg": ["hugo"], "configs": []}, + + # ── PHP ── + {"name": "PHP-FPM", "cat": "runtime", "process": ["php-fpm"], "ports": [9000], "pkg": ["php-fpm", "php8.1-fpm", "php8.2-fpm", "php8.3-fpm"], "configs": ["/etc/php/", "/etc/php/8.2/fpm/php-fpm.conf", "/etc/php/8.2/fpm/pool.d/www.conf"]}, + + # ── App Runtimes ── + {"name": "Node.js (PM2)", "cat": "runtime", "process": ["PM2", "node"], "ports": [], "pkg": ["nodejs"], "configs": ["/etc/pm2/", "ecosystem.config.js"]}, + {"name": "Python (Gunicorn)", "cat": "runtime", "process": ["gunicorn"], "ports": [8000], "pkg": ["gunicorn"], "configs": []}, + {"name": "Python (uWSGI)", "cat": "runtime", "process": ["uwsgi"], "ports": [], "pkg": ["uwsgi"], "configs": ["/etc/uwsgi/apps-enabled/"]}, + {"name": "Tomcat", "cat": "runtime", "process": ["java.*catalina"], "ports": [8080], "pkg": ["tomcat9", "tomcat10"], "configs": ["/etc/tomcat9/server.xml", "/etc/tomcat10/server.xml"]}, + {"name": "Supervisor", "cat": "runtime", "process": ["supervisord"], "ports": [9001], "pkg": ["supervisor"], "configs": ["/etc/supervisor/supervisord.conf", "/etc/supervisor/conf.d/"]}, + + # ── Message Queues ── + {"name": "RabbitMQ", "cat": "mq", "process": ["beam.smp", "rabbitmq"], "ports": [5672, 15672], "pkg": ["rabbitmq-server"], "configs": ["/etc/rabbitmq/rabbitmq.conf"]}, + {"name": "Mosquitto (MQTT)", "cat": "mq", "process": ["mosquitto"], "ports": [1883, 8883], "pkg": ["mosquitto"], "configs": ["/etc/mosquitto/mosquitto.conf"]}, + {"name": "NATS", "cat": "mq", "process": ["nats-server"], "ports": [4222, 8222], "pkg": [], "configs": ["/etc/nats/nats-server.conf"]}, + {"name": "Kafka", "cat": "mq", "process": ["java.*kafka"], "ports": [9092], "pkg": [], "configs": ["/etc/kafka/server.properties"]}, + {"name": "ZeroMQ", "cat": "mq", "process": [], "ports": [], "pkg": ["libzmq5"], "configs": []}, + + # ── Chat / Communication ── + {"name": "Matrix (Synapse)", "cat": "chat", "process": ["synapse", "python.*synapse"], "ports": [8008, 8448], "pkg": ["matrix-synapse"], "configs": ["/etc/matrix-synapse/homeserver.yaml"]}, + {"name": "Mattermost", "cat": "chat", "process": ["mattermost"], "ports": [8065], "pkg": [], "configs": ["/opt/mattermost/config/config.json"]}, + {"name": "Rocket.Chat", "cat": "chat", "process": ["rocketchat"], "ports": [3000], "pkg": [], "configs": []}, + {"name": "XMPP (Prosody)", "cat": "chat", "process": ["prosody"], "ports": [5222, 5269], "pkg": ["prosody"], "configs": ["/etc/prosody/prosody.cfg.lua"]}, + {"name": "XMPP (ejabberd)", "cat": "chat", "process": ["ejabberd"], "ports": [5222, 5269], "pkg": ["ejabberd"], "configs": ["/etc/ejabberd/ejabberd.yml"]}, + {"name": "Mumble", "cat": "chat", "process": ["murmurd"], "ports": [64738], "pkg": ["mumble-server"], "configs": ["/etc/mumble-server.ini"]}, + {"name": "TeamSpeak", "cat": "chat", "process": ["ts3server"], "ports": [9987, 30033], "pkg": [], "configs": ["/opt/teamspeak/ts3server.ini"]}, + {"name": "Jitsi Meet", "cat": "chat", "process": ["jicofo", "jvb"], "ports": [80, 443, 10000], "pkg": ["jitsi-meet"], "configs": ["/etc/jitsi/meet/"]}, + {"name": "Gotify", "cat": "chat", "process": ["gotify"], "ports": [80], "pkg": [], "configs": ["/etc/gotify/config.yml"]}, + {"name": "ntfy", "cat": "chat", "process": ["ntfy"], "ports": [80], "pkg": ["ntfy"], "configs": ["/etc/ntfy/server.yml"]}, + + # ── File Sharing / Cloud ── + {"name": "Nextcloud", "cat": "cloud", "process": [], "ports": [], "pkg": ["nextcloud"], "configs": ["/var/www/nextcloud/config/config.php"]}, + {"name": "Seafile", "cat": "cloud", "process": ["seafile-controller", "seaf-server"], "ports": [8000, 8082], "pkg": [], "configs": ["/opt/seafile/conf/"]}, + {"name": "Syncthing", "cat": "cloud", "process": ["syncthing"], "ports": [8384, 22000], "pkg": ["syncthing"], "configs": ["/etc/syncthing/"]}, + {"name": "MinIO", "cat": "cloud", "process": ["minio"], "ports": [9000, 9001], "pkg": [], "configs": ["/etc/minio/config.env"]}, + {"name": "Samba", "cat": "cloud", "process": ["smbd", "nmbd"], "ports": [139, 445], "pkg": ["samba"], "configs": ["/etc/samba/smb.conf"]}, + {"name": "NFS", "cat": "cloud", "process": ["nfsd", "rpc.mountd"], "ports": [2049], "pkg": ["nfs-kernel-server"], "configs": ["/etc/exports"]}, + {"name": "WebDAV", "cat": "cloud", "process": [], "ports": [], "pkg": [], "configs": []}, + {"name": "FileBrowser", "cat": "cloud", "process": ["filebrowser"], "ports": [8080], "pkg": [], "configs": ["/etc/filebrowser/filebrowser.json"]}, + + # ── Media ── + {"name": "Jellyfin", "cat": "media", "process": ["jellyfin"], "ports": [8096], "pkg": ["jellyfin"], "configs": ["/etc/jellyfin/system.xml"]}, + {"name": "Plex", "cat": "media", "process": ["Plex Media Server"], "ports": [32400], "pkg": ["plexmediaserver"], "configs": ["/var/lib/plexmediaserver/"]}, + {"name": "Emby", "cat": "media", "process": ["emby"], "ports": [8096], "pkg": ["emby-server"], "configs": ["/etc/emby-server.conf"]}, + {"name": "Navidrome", "cat": "media", "process": ["navidrome"], "ports": [4533], "pkg": [], "configs": ["/etc/navidrome/navidrome.toml"]}, + {"name": "Icecast", "cat": "media", "process": ["icecast2"], "ports": [8000], "pkg": ["icecast2"], "configs": ["/etc/icecast2/icecast.xml"]}, + + # ── Automation ── + {"name": "n8n", "cat": "automation", "process": ["n8n"], "ports": [5678], "pkg": [], "configs": []}, + {"name": "Node-RED", "cat": "automation", "process": ["node-red"], "ports": [1880], "pkg": [], "configs": ["~/.node-red/settings.js"]}, + {"name": "Ansible", "cat": "automation", "process": [], "ports": [], "pkg": ["ansible"], "configs": ["/etc/ansible/ansible.cfg", "/etc/ansible/hosts"]}, + {"name": "Cron", "cat": "automation", "process": ["cron", "crond"], "ports": [], "pkg": ["cron"], "configs": ["/etc/crontab", "/etc/cron.d/"]}, + {"name": "systemd-timers", "cat": "automation", "process": [], "ports": [], "pkg": [], "configs": []}, + {"name": "at", "cat": "automation", "process": ["atd"], "ports": [], "pkg": ["at"], "configs": []}, + + # ── Analytics ── + {"name": "Matomo", "cat": "analytics", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/matomo/config/config.ini.php"]}, + {"name": "Plausible", "cat": "analytics", "process": ["plausible"], "ports": [8000], "pkg": [], "configs": []}, + {"name": "Umami", "cat": "analytics", "process": ["umami"], "ports": [3000], "pkg": [], "configs": []}, + {"name": "GoAccess", "cat": "analytics", "process": ["goaccess"], "ports": [7890], "pkg": ["goaccess"], "configs": ["/etc/goaccess/goaccess.conf"]}, + {"name": "AWStats", "cat": "analytics", "process": [], "ports": [], "pkg": ["awstats"], "configs": ["/etc/awstats/"]}, + + # ── Backup ── + {"name": "BorgBackup", "cat": "backup", "process": ["borg"], "ports": [], "pkg": ["borgbackup"], "configs": []}, + {"name": "Restic", "cat": "backup", "process": ["restic"], "ports": [], "pkg": ["restic"], "configs": []}, + {"name": "Duplicity", "cat": "backup", "process": ["duplicity"], "ports": [], "pkg": ["duplicity"], "configs": []}, + {"name": "Bacula", "cat": "backup", "process": ["bacula-dir", "bacula-sd", "bacula-fd"], "ports": [9101, 9102, 9103], "pkg": ["bacula-director", "bacula-sd", "bacula-fd"], "configs": ["/etc/bacula/bacula-dir.conf", "/etc/bacula/bacula-sd.conf"]}, + {"name": "rsnapshot", "cat": "backup", "process": [], "ports": [], "pkg": ["rsnapshot"], "configs": ["/etc/rsnapshot.conf"]}, + {"name": "Duplicati", "cat": "backup", "process": ["duplicati"], "ports": [8200], "pkg": [], "configs": []}, + + # ── Mailing Lists ── + {"name": "Listmonk", "cat": "mail", "process": ["listmonk"], "ports": [9000], "pkg": [], "configs": ["/etc/listmonk/config.toml"]}, + {"name": "Mailman", "cat": "mail", "process": ["mailman"], "ports": [8001], "pkg": ["mailman3"], "configs": ["/etc/mailman3/"]}, + {"name": "Sympa", "cat": "mail", "process": ["sympa"], "ports": [], "pkg": ["sympa"], "configs": ["/etc/sympa/sympa.conf"]}, + + # ── Misc Services ── + {"name": "Certbot", "cat": "misc", "process": [], "ports": [], "pkg": ["certbot"], "configs": ["/etc/letsencrypt/"]}, + {"name": "ACME.sh", "cat": "misc", "process": [], "ports": [], "pkg": [], "configs": ["~/.acme.sh/"]}, + {"name": "logrotate", "cat": "misc", "process": [], "ports": [], "pkg": ["logrotate"], "configs": ["/etc/logrotate.conf", "/etc/logrotate.d/"]}, + {"name": "NTP (chrony)", "cat": "misc", "process": ["chronyd"], "ports": [123], "pkg": ["chrony"], "configs": ["/etc/chrony/chrony.conf"]}, + {"name": "NTP (ntpd)", "cat": "misc", "process": ["ntpd"], "ports": [123], "pkg": ["ntp"], "configs": ["/etc/ntp.conf"]}, + {"name": "systemd-resolved", "cat": "misc", "process": ["systemd-resolved"], "ports": [53], "pkg": [], "configs": ["/etc/systemd/resolved.conf"]}, + {"name": "snapd", "cat": "misc", "process": ["snapd"], "ports": [], "pkg": ["snapd"], "configs": []}, + {"name": "CUPS", "cat": "misc", "process": ["cupsd"], "ports": [631], "pkg": ["cups"], "configs": ["/etc/cups/cupsd.conf"]}, + {"name": "Tor", "cat": "misc", "process": ["tor"], "ports": [9050, 9051], "pkg": ["tor"], "configs": ["/etc/tor/torrc"]}, + {"name": "Privoxy", "cat": "misc", "process": ["privoxy"], "ports": [8118], "pkg": ["privoxy"], "configs": ["/etc/privoxy/config"]}, + {"name": "DNSCrypt", "cat": "misc", "process": ["dnscrypt-proxy"], "ports": [53], "pkg": ["dnscrypt-proxy"], "configs": ["/etc/dnscrypt-proxy/dnscrypt-proxy.toml"]}, + {"name": "Consul", "cat": "misc", "process": ["consul"], "ports": [8500, 8600], "pkg": ["consul"], "configs": ["/etc/consul.d/"]}, + {"name": "Vaultwarden", "cat": "misc", "process": ["vaultwarden"], "ports": [80, 3012], "pkg": [], "configs": ["/etc/vaultwarden.env", "/opt/vaultwarden/.env"]}, + + # ── Password / Secrets ── + {"name": "Bitwarden", "cat": "auth", "process": ["bitwarden"], "ports": [80, 443], "pkg": [], "configs": ["/opt/bitwarden/bwdata/config.yml"]}, + + # ── Status Pages ── + {"name": "Cachet", "cat": "monitor", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/cachet/.env"]}, + {"name": "Statping", "cat": "monitor", "process": ["statping"], "ports": [8080], "pkg": [], "configs": []}, + + # ── Pastebin / Snippets ── + {"name": "PrivateBin", "cat": "misc", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/privatebin/cfg/conf.php"]}, + {"name": "Hastebin", "cat": "misc", "process": ["haste-server"], "ports": [7777], "pkg": [], "configs": []}, + + # ── URL Shorteners ── + {"name": "YOURLS", "cat": "misc", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/yourls/user/config.php"]}, + {"name": "Shlink", "cat": "misc", "process": ["shlink"], "ports": [8080], "pkg": [], "configs": []}, + + # ── Forums ── + {"name": "Discourse", "cat": "cms", "process": ["discourse"], "ports": [80], "pkg": [], "configs": ["/var/discourse/containers/app.yml"]}, + {"name": "Flarum", "cat": "cms", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/flarum/config.php"]}, + {"name": "phpBB", "cat": "cms", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/phpbb/config.php"]}, + + # ── Project Management ── + {"name": "Wekan", "cat": "misc", "process": ["wekan"], "ports": [80], "pkg": [], "configs": []}, + {"name": "Kanboard", "cat": "misc", "process": [], "ports": [], "pkg": [], "configs": ["/var/www/kanboard/config.php"]}, + {"name": "Taiga", "cat": "misc", "process": ["taiga"], "ports": [80], "pkg": [], "configs": ["/opt/taiga/config.py"]}, + {"name": "Gitea Act Runner", "cat": "ci", "process": ["act_runner"], "ports": [], "pkg": [], "configs": []}, +] + +CATEGORIES = { + "web": "Web Servers", + "remote": "Remote Access", + "ftp": "FTP / File Transfer", + "dns": "DNS", + "mail": "Mail", + "db": "Databases", + "dbadmin": "DB Admin Tools", + "git": "Git / Code Forges", + "ci": "CI/CD", + "container": "Containers", + "monitor": "Monitoring", + "vpn": "VPN / Tunnels", + "security": "Firewall / Security", + "auth": "Auth / SSO", + "cms": "CMS / Web Apps", + "runtime": "App Runtimes", + "mq": "Message Queues", + "chat": "Chat / Communication", + "cloud": "File Sharing / Cloud", + "media": "Media", + "automation": "Automation", + "analytics": "Analytics", + "backup": "Backup", + "misc": "Miscellaneous", +} + + +def build_detect_command(): + """Build a single SSH command that gathers all detection data at once.""" + return ( + "echo '===PROCESSES===' && ps aux --no-headers 2>/dev/null | awk '{print $11}' | sort -u && " + "echo '===LISTENING===' && ss -tlnp 2>/dev/null | awk 'NR>1{print $4}' | grep -oP '\\d+$' | sort -un && " + "echo '===PACKAGES===' && (dpkg -l 2>/dev/null | awk '/^ii/{print $2}' || rpm -qa --qf '%{NAME}\\n' 2>/dev/null) | sort -u && " + "echo '===CONFIGS===' && ls -1d " + "/etc/nginx/ /etc/apache2/ /etc/caddy/ /etc/haproxy/ " + "/etc/ssh/ /etc/vsftpd* /etc/proftpd/ " + "/etc/bind/ /etc/unbound/ /etc/dnsmasq* /etc/pihole/ " + "/etc/postfix/ /etc/exim4/ /etc/dovecot/ /etc/opendkim* " + "/etc/mysql/ /etc/postgresql/ /etc/redis/ /etc/mongod* /etc/influxdb/ /etc/elasticsearch/ " + "/etc/gitlab/ /etc/gitea/ /etc/forgejo/ " + "/etc/docker/ /etc/lxc/ " + "/etc/prometheus/ /etc/grafana/ /etc/zabbix/ /etc/netdata/ /etc/nagios*/ " + "/etc/wireguard/ /etc/openvpn/ " + "/etc/fail2ban/ /etc/crowdsec/ /etc/ufw/ /etc/iptables/ /etc/nftables* " + "/etc/letsencrypt/ /etc/logrotate* " + "/etc/php/ /etc/supervisor/ " + "/etc/rabbitmq/ /etc/mosquitto/ " + "/etc/samba/ /etc/exports " + "/etc/tor/ /etc/consul* " + "/var/www/nextcloud/ /var/www/wordpress/ /var/www/matomo/ " + "/opt/mattermost/ /opt/keycloak/ /opt/seafile/ " + "2>/dev/null && " + "echo '===DOCKER===' && docker ps -a --format '{{.Names}}|{{.Image}}' 2>/dev/null && " + "echo '===SYSTEMD===' && systemctl list-units --type=service --state=running --no-pager --no-legend 2>/dev/null | awk '{print $1}' && " + "echo '===END==='" + ) + + +def parse_detection(raw_output): + """Parse the detection output and match against known services.""" + sections = {} + current = None + for line in raw_output.split("\n"): + line = line.strip() + if line.startswith("===") and line.endswith("==="): + current = line.strip("=") + sections[current] = [] + elif current and line: + sections[current].append(line) + + processes = set(sections.get("PROCESSES", [])) + ports = set() + for p in sections.get("LISTENING", []): + try: + ports.add(int(p)) + except ValueError: + pass + packages = set(sections.get("PACKAGES", [])) + configs_found = set(sections.get("CONFIGS", [])) + docker_containers = sections.get("DOCKER", []) + systemd_units = set(sections.get("SYSTEMD", [])) + + detected = [] + + for svc in SERVICES: + score = 0 + evidence = [] + + # Check processes + for proc in svc["process"]: + for running in processes: + if proc in running: + score += 3 + evidence.append(f"process: {running}") + break + + # Check systemd units + for proc in svc["process"]: + for unit in systemd_units: + if proc in unit: + score += 2 + evidence.append(f"systemd: {unit}") + break + + # Check ports + for port in svc["ports"]: + if port in ports: + score += 2 + evidence.append(f"port: {port}") + + # Check packages + for pkg in svc["pkg"]: + # packages often have version suffixes, so check prefix + for installed in packages: + if installed == pkg or installed.startswith(pkg + ":"): + score += 3 + evidence.append(f"package: {installed}") + break + + # Check config files + for cfg in svc["configs"]: + for found in configs_found: + if found.startswith(cfg.rstrip("/")): + score += 2 + evidence.append(f"config: {found}") + break + + # Check docker containers + for dc in docker_containers: + name_lower = dc.lower() + svc_lower = svc["name"].lower().replace(" ", "").replace("-", "") + if svc_lower in name_lower: + score += 3 + evidence.append(f"docker: {dc}") + + if score >= 2: + detected.append({ + "name": svc["name"], + "category": CATEGORIES.get(svc["cat"], svc["cat"]), + "cat": svc["cat"], + "score": score, + "evidence": evidence, + "configs": svc["configs"], + "ports": svc["ports"], + }) + + detected.sort(key=lambda x: (-x["score"], x["name"])) + return detected diff --git a/setec-web/dns_client.py b/setec-web/dns_client.py new file mode 100644 index 0000000..d1930a8 --- /dev/null +++ b/setec-web/dns_client.py @@ -0,0 +1,110 @@ +import requests +import ssh_client +import config + +BASE = "https://api.hostinger.com/api/dns/v1/zones" + +def _headers(): + cfg = config.load() + return { + "Authorization": f"Bearer {cfg['hostinger_api_key']}", + "Content-Type": "application/json", + } + +def _domain(): + return config.load()["domain"] + +def get_records(): + """Try Hostinger API first (via VPS to avoid Cloudflare), fall back to dig.""" + cfg = config.load() + key = cfg.get("hostinger_api_key", "") + domain = cfg["domain"] + + # Try API via VPS + try: + cmd = ( + f"curl -s -w '\\nHTTP_CODE:%{{http_code}}' " + f"-H 'Authorization: Bearer {key}' " + f"-H 'Content-Type: application/json' " + f"'https://api.hostinger.com/api/dns/v1/zones/{domain}' 2>&1" + ) + result = ssh_client.run(cmd, timeout=15) + output = result["stdout"] + if "HTTP_CODE:200" in output: + import json + body = output.rsplit("\nHTTP_CODE:", 1)[0] + return json.loads(body) + except Exception: + pass + + # Try direct from local + try: + r = requests.get(f"{BASE}/{domain}", headers=_headers(), timeout=15) + if r.status_code == 200: + return r.json() + except Exception: + pass + + # Fallback: use dig to show current DNS records + result = ssh_client.run( + f"echo '=== A Records ===' && dig +short A {domain} && " + f"echo '=== CNAME ===' && dig +short CNAME {domain} && " + f"echo '=== MX ===' && dig +short MX {domain} && " + f"echo '=== TXT ===' && dig +short TXT {domain} && " + f"echo '=== NS ===' && dig +short NS {domain} && " + f"echo '=== Subdomains ===' && " + f"for sub in repo git app files lists mail www; do " + f" ip=$(dig +short A $sub.{domain}); " + f" [ -n \"$ip\" ] && echo \"$sub.{domain} -> $ip\"; " + f"done", + timeout=15, + ) + return {"source": "dig (API unavailable)", "records": result["stdout"]} + + +def add_a_record(name, ip, ttl=14400): + cfg = config.load() + key = cfg.get("hostinger_api_key", "") + domain = cfg["domain"] + payload = f'{{"records":[{{"name":"{name}","type":"A","content":"{ip}","ttl":{ttl}}}],"overwrite":false}}' + + result = ssh_client.run( + f"curl -s -w '\\nHTTP_CODE:%{{http_code}}' -X PUT " + f"-H 'Authorization: Bearer {key}' " + f"-H 'Content-Type: application/json' " + f"-d '{payload}' " + f"'https://api.hostinger.com/api/dns/v1/zones/{domain}' 2>&1", + timeout=15, + ) + return {"response": result["stdout"]} + + +def add_txt_record(name, value, ttl=14400): + cfg = config.load() + key = cfg.get("hostinger_api_key", "") + domain = cfg["domain"] + payload = f'{{"records":[{{"name":"{name}","type":"TXT","content":"{value}","ttl":{ttl}}}],"overwrite":false}}' + + result = ssh_client.run( + f"curl -s -w '\\nHTTP_CODE:%{{http_code}}' -X PUT " + f"-H 'Authorization: Bearer {key}' " + f"-H 'Content-Type: application/json' " + f"-d '{payload}' " + f"'https://api.hostinger.com/api/dns/v1/zones/{domain}' 2>&1", + timeout=15, + ) + return {"response": result["stdout"]} + + +def delete_record(record_id): + cfg = config.load() + key = cfg.get("hostinger_api_key", "") + domain = cfg["domain"] + + result = ssh_client.run( + f"curl -s -w '\\nHTTP_CODE:%{{http_code}}' -X DELETE " + f"-H 'Authorization: Bearer {key}' " + f"'https://api.hostinger.com/api/dns/v1/zones/{domain}/records/{record_id}' 2>&1", + timeout=15, + ) + return {"response": result["stdout"]} diff --git a/setec-web/docker_store.py b/setec-web/docker_store.py new file mode 100644 index 0000000..c7d4ca1 --- /dev/null +++ b/setec-web/docker_store.py @@ -0,0 +1,709 @@ +# Docker app store - popular self-hosted apps with one-click install +# Each entry has the docker-compose snippet needed to deploy it + +STORE = [ + # ── Web / Proxy ── + { + "name": "Nginx Proxy Manager", + "desc": "Web UI for managing Nginx reverse proxies with SSL", + "cat": "Web / Proxy", + "image": "jc21/nginx-proxy-manager:latest", + "ports": {"81": "Admin UI", "80": "HTTP", "443": "HTTPS"}, + "ui_port": 81, + "compose": """ nginx-proxy-manager: + image: jc21/nginx-proxy-manager:latest + container_name: nginx-proxy-manager + restart: always + ports: + - "81:81" + - "8880:80" + - "8443:443" + volumes: + - /opt/seteclabs/npm/data:/data + - /opt/seteclabs/npm/letsencrypt:/etc/letsencrypt""", + "notes": "Default login: admin@example.com / changeme", + }, + { + "name": "Traefik", + "desc": "Cloud-native reverse proxy and load balancer", + "cat": "Web / Proxy", + "image": "traefik:latest", + "ports": {"8080": "Dashboard"}, + "ui_port": 8080, + "compose": """ traefik: + image: traefik:latest + container_name: traefik + restart: always + command: + - "--api.dashboard=true" + - "--api.insecure=true" + - "--providers.docker=true" + ports: + - "8880:80" + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro""", + "notes": "Dashboard at :8080, configure via labels on other containers", + }, + + # ── Monitoring ── + { + "name": "Uptime Kuma", + "desc": "Self-hosted uptime monitoring with beautiful status pages", + "cat": "Monitoring", + "image": "louislam/uptime-kuma:latest", + "ports": {"3001": "Web UI"}, + "ui_port": 3001, + "compose": """ uptime-kuma: + image: louislam/uptime-kuma:latest + container_name: uptime-kuma + restart: always + ports: + - "3001:3001" + volumes: + - /opt/seteclabs/uptime-kuma:/app/data""", + "notes": "Monitor websites, APIs, DNS, and more. Create public status pages.", + }, + { + "name": "Grafana", + "desc": "Dashboards and visualization for metrics", + "cat": "Monitoring", + "image": "grafana/grafana:latest", + "ports": {"3002": "Web UI"}, + "ui_port": 3002, + "compose": """ grafana: + image: grafana/grafana:latest + container_name: grafana + restart: always + ports: + - "3002:3000" + volumes: + - /opt/seteclabs/grafana:/var/lib/grafana""", + "notes": "Default login: admin / admin", + }, + { + "name": "Prometheus", + "desc": "Metrics collection and alerting", + "cat": "Monitoring", + "image": "prom/prometheus:latest", + "ports": {"9090": "Web UI"}, + "ui_port": 9090, + "compose": """ prometheus: + image: prom/prometheus:latest + container_name: prometheus + restart: always + ports: + - "9090:9090" + volumes: + - /opt/seteclabs/prometheus:/prometheus + - /opt/seteclabs/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml""", + "notes": "Create prometheus.yml config before starting", + }, + { + "name": "Netdata", + "desc": "Real-time server performance monitoring", + "cat": "Monitoring", + "image": "netdata/netdata:latest", + "ports": {"19999": "Web UI"}, + "ui_port": 19999, + "compose": """ netdata: + image: netdata/netdata:latest + container_name: netdata + restart: always + ports: + - "19999:19999" + cap_add: + - SYS_PTRACE + security_opt: + - apparmor:unconfined + volumes: + - /etc/passwd:/host/etc/passwd:ro + - /etc/group:/host/etc/group:ro + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /var/run/docker.sock:/var/run/docker.sock:ro""", + "notes": "Full system metrics, zero config needed", + }, + + # ── Containers ── + { + "name": "Portainer", + "desc": "Docker management web UI", + "cat": "Containers", + "image": "portainer/portainer-ce:latest", + "ports": {"9443": "Web UI (HTTPS)", "9000": "Web UI (HTTP)"}, + "ui_port": 9000, + "compose": """ portainer: + image: portainer/portainer-ce:latest + container_name: portainer + restart: always + ports: + - "9443:9443" + - "9002:9000" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /opt/seteclabs/portainer:/data""", + "notes": "Docker management UI. Set admin password on first visit.", + }, + { + "name": "Dockge", + "desc": "Compose stack manager with live editing", + "cat": "Containers", + "image": "louislam/dockge:latest", + "ports": {"5001": "Web UI"}, + "ui_port": 5001, + "compose": """ dockge: + image: louislam/dockge:latest + container_name: dockge + restart: always + ports: + - "5001:5001" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /opt/seteclabs/dockge:/app/data + - /opt/seteclabs:/opt/seteclabs""", + "notes": "Manage docker compose stacks from a web UI", + }, + + # ── Cloud / Files ── + { + "name": "Nextcloud", + "desc": "Self-hosted cloud storage (Google Drive/Dropbox alternative)", + "cat": "Cloud / Files", + "image": "nextcloud:latest", + "ports": {"8081": "Web UI"}, + "ui_port": 8081, + "compose": """ nextcloud: + image: nextcloud:latest + container_name: nextcloud + restart: always + ports: + - "8081:80" + volumes: + - /opt/seteclabs/nextcloud:/var/www/html""", + "notes": "File sync, calendar, contacts, office docs", + }, + { + "name": "FileBrowser", + "desc": "Web-based file manager", + "cat": "Cloud / Files", + "image": "filebrowser/filebrowser:latest", + "ports": {"8082": "Web UI"}, + "ui_port": 8082, + "compose": """ filebrowser: + image: filebrowser/filebrowser:latest + container_name: filebrowser + restart: always + ports: + - "8082:80" + volumes: + - /var/www:/srv + - /opt/seteclabs/filebrowser/db:/database""", + "notes": "Default login: admin / admin. Browses /var/www by default.", + }, + { + "name": "MinIO", + "desc": "S3-compatible object storage", + "cat": "Cloud / Files", + "image": "minio/minio:latest", + "ports": {"9010": "API", "9011": "Console"}, + "ui_port": 9011, + "compose": """ minio: + image: minio/minio:latest + container_name: minio + restart: always + command: server /data --console-address ":9001" + ports: + - "9010:9000" + - "9011:9001" + environment: + - MINIO_ROOT_USER=admin + - MINIO_ROOT_PASSWORD=setecminio2026 + volumes: + - /opt/seteclabs/minio:/data""", + "notes": "S3-compatible storage. Console at :9011", + }, + { + "name": "Syncthing", + "desc": "Continuous file synchronization between devices", + "cat": "Cloud / Files", + "image": "syncthing/syncthing:latest", + "ports": {"8384": "Web UI"}, + "ui_port": 8384, + "compose": """ syncthing: + image: syncthing/syncthing:latest + container_name: syncthing + restart: always + ports: + - "8384:8384" + - "22000:22000/tcp" + - "22000:22000/udp" + volumes: + - /opt/seteclabs/syncthing:/var/syncthing""", + "notes": "Peer-to-peer file sync, no cloud needed", + }, + + # ── Security ── + { + "name": "Vaultwarden", + "desc": "Bitwarden-compatible password manager", + "cat": "Security", + "image": "vaultwarden/server:latest", + "ports": {"8083": "Web UI"}, + "ui_port": 8083, + "compose": """ vaultwarden: + image: vaultwarden/server:latest + container_name: vaultwarden + restart: always + ports: + - "8083:80" + environment: + - SIGNUPS_ALLOWED=false + - ADMIN_TOKEN=setecVault2026adminToken + volumes: + - /opt/seteclabs/vaultwarden:/data""", + "notes": "Use with Bitwarden browser extension. Admin panel at /admin", + }, + { + "name": "CrowdSec", + "desc": "Collaborative security engine (fail2ban alternative)", + "cat": "Security", + "image": "crowdsecurity/crowdsec:latest", + "ports": {"8084": "API"}, + "ui_port": 8084, + "compose": """ crowdsec: + image: crowdsecurity/crowdsec:latest + container_name: crowdsec + restart: always + ports: + - "8084:8080" + volumes: + - /opt/seteclabs/crowdsec/config:/etc/crowdsec + - /opt/seteclabs/crowdsec/data:/var/lib/crowdsec/data + - /var/log:/var/log:ro""", + "notes": "Community-driven IP reputation and threat detection", + }, + + # ── Communication ── + { + "name": "Gotify", + "desc": "Self-hosted push notification server", + "cat": "Communication", + "image": "gotify/server:latest", + "ports": {"8085": "Web UI"}, + "ui_port": 8085, + "compose": """ gotify: + image: gotify/server:latest + container_name: gotify + restart: always + ports: + - "8085:80" + volumes: + - /opt/seteclabs/gotify:/app/data""", + "notes": "Default login: admin / admin", + }, + { + "name": "ntfy", + "desc": "Simple pub-sub push notifications via HTTP", + "cat": "Communication", + "image": "binwiederhier/ntfy:latest", + "ports": {"8086": "Web UI"}, + "ui_port": 8086, + "compose": """ ntfy: + image: binwiederhier/ntfy:latest + container_name: ntfy + restart: always + command: serve + ports: + - "8086:80" + volumes: + - /opt/seteclabs/ntfy/cache:/var/cache/ntfy + - /opt/seteclabs/ntfy/etc:/etc/ntfy""", + "notes": "Send notifications via curl: curl -d 'message' http://host:8086/topic", + }, + { + "name": "Element Web", + "desc": "Matrix chat client (Slack/Discord alternative)", + "cat": "Communication", + "image": "vectorim/element-web:latest", + "ports": {"8087": "Web UI"}, + "ui_port": 8087, + "compose": """ element: + image: vectorim/element-web:latest + container_name: element + restart: always + ports: + - "8087:80" """, + "notes": "Needs a Matrix homeserver (Synapse/Dendrite) to connect to", + }, + + # ── Analytics ── + { + "name": "Plausible", + "desc": "Privacy-friendly web analytics (Google Analytics alternative)", + "cat": "Analytics", + "image": "plausible/analytics:latest", + "ports": {"8088": "Web UI"}, + "ui_port": 8088, + "compose": """ plausible: + image: plausible/analytics:latest + container_name: plausible + restart: always + ports: + - "8088:8000" + environment: + - BASE_URL=http://localhost:8088 + - SECRET_KEY_BASE=setecPlaus2026secretKeyBase1234567890abcdef + - DATABASE_URL=postgres://plausible:plausible@plausible-db/plausible + - CLICKHOUSE_DATABASE_URL=http://plausible-events-db:8123/plausible_events_db + depends_on: + - plausible-db + - plausible-events-db + + plausible-db: + image: postgres:16-alpine + container_name: plausible-db + restart: always + environment: + - POSTGRES_USER=plausible + - POSTGRES_PASSWORD=plausible + - POSTGRES_DB=plausible + volumes: + - /opt/seteclabs/plausible/pgdata:/var/lib/postgresql/data + + plausible-events-db: + image: clickhouse/clickhouse-server:latest + container_name: plausible-events-db + restart: always + volumes: + - /opt/seteclabs/plausible/clickhouse:/var/lib/clickhouse""", + "notes": "Lightweight, no cookies, GDPR-compliant analytics", + }, + { + "name": "Umami", + "desc": "Simple privacy-focused web analytics", + "cat": "Analytics", + "image": "ghcr.io/umami-software/umami:postgresql-latest", + "ports": {"8089": "Web UI"}, + "ui_port": 8089, + "compose": """ umami: + image: ghcr.io/umami-software/umami:postgresql-latest + container_name: umami + restart: always + ports: + - "8089:3000" + environment: + - DATABASE_URL=postgresql://umami:umami@umami-db:5432/umami + depends_on: + - umami-db + + umami-db: + image: postgres:16-alpine + container_name: umami-db + restart: always + environment: + - POSTGRES_USER=umami + - POSTGRES_PASSWORD=umami + - POSTGRES_DB=umami + volumes: + - /opt/seteclabs/umami/pgdata:/var/lib/postgresql/data""", + "notes": "Default login: admin / umami", + }, + + # ── Wikis / Knowledge ── + { + "name": "Wiki.js", + "desc": "Modern wiki with markdown, visual editor, and Git sync", + "cat": "Wikis", + "image": "ghcr.io/requarks/wiki:2", + "ports": {"3003": "Web UI"}, + "ui_port": 3003, + "compose": """ wikijs: + image: ghcr.io/requarks/wiki:2 + container_name: wikijs + restart: always + ports: + - "3003:3000" + environment: + - DB_TYPE=sqlite + - DB_FILEPATH=/data/wiki.sqlite + volumes: + - /opt/seteclabs/wikijs:/data""", + "notes": "Setup wizard on first launch", + }, + { + "name": "BookStack", + "desc": "Documentation platform with books/chapters/pages", + "cat": "Wikis", + "image": "lscr.io/linuxserver/bookstack:latest", + "ports": {"6875": "Web UI"}, + "ui_port": 6875, + "compose": """ bookstack: + image: lscr.io/linuxserver/bookstack:latest + container_name: bookstack + restart: always + ports: + - "6875:80" + environment: + - PUID=1000 + - PGID=1000 + - APP_URL=http://localhost:6875 + - DB_HOST=bookstack-db + - DB_PORT=3306 + - DB_USER=bookstack + - DB_PASS=bookstack + - DB_DATABASE=bookstackapp + depends_on: + - bookstack-db + + bookstack-db: + image: mariadb:10 + container_name: bookstack-db + restart: always + environment: + - MYSQL_ROOT_PASSWORD=bookstack + - MYSQL_DATABASE=bookstackapp + - MYSQL_USER=bookstack + - MYSQL_PASSWORD=bookstack + volumes: + - /opt/seteclabs/bookstack/db:/var/lib/mysql""", + "notes": "Default login: admin@admin.com / password", + }, + + # ── Automation ── + { + "name": "n8n", + "desc": "Workflow automation (Zapier/IFTTT alternative)", + "cat": "Automation", + "image": "n8nio/n8n:latest", + "ports": {"5678": "Web UI"}, + "ui_port": 5678, + "compose": """ n8n: + image: n8nio/n8n:latest + container_name: n8n + restart: always + ports: + - "5678:5678" + volumes: + - /opt/seteclabs/n8n:/home/node/.n8n""", + "notes": "Visual workflow builder with 200+ integrations", + }, + { + "name": "Node-RED", + "desc": "Flow-based programming for IoT and automation", + "cat": "Automation", + "image": "nodered/node-red:latest", + "ports": {"1880": "Web UI"}, + "ui_port": 1880, + "compose": """ nodered: + image: nodered/node-red:latest + container_name: nodered + restart: always + ports: + - "1880:1880" + volumes: + - /opt/seteclabs/nodered:/data""", + "notes": "Drag-and-drop flow editor", + }, + + # ── Pastebins / Tools ── + { + "name": "PrivateBin", + "desc": "Encrypted pastebin (zero-knowledge)", + "cat": "Tools", + "image": "privatebin/nginx-fpm-alpine:latest", + "ports": {"8090": "Web UI"}, + "ui_port": 8090, + "compose": """ privatebin: + image: privatebin/nginx-fpm-alpine:latest + container_name: privatebin + restart: always + ports: + - "8090:8080" + volumes: + - /opt/seteclabs/privatebin:/srv/data""", + "notes": "Encrypted pastes, server never sees plaintext", + }, + { + "name": "IT-Tools", + "desc": "Collection of handy developer tools (encoders, converters, etc)", + "cat": "Tools", + "image": "corentinth/it-tools:latest", + "ports": {"8091": "Web UI"}, + "ui_port": 8091, + "compose": """ it-tools: + image: corentinth/it-tools:latest + container_name: it-tools + restart: always + ports: + - "8091:80" """, + "notes": "Hash generators, encoders, UUID, JWT decoder, network tools, etc", + }, + { + "name": "CyberChef", + "desc": "Data encoding/decoding/analysis swiss army knife", + "cat": "Tools", + "image": "ghcr.io/gchq/cyberchef:latest", + "ports": {"8092": "Web UI"}, + "ui_port": 8092, + "compose": """ cyberchef: + image: ghcr.io/gchq/cyberchef:latest + container_name: cyberchef + restart: always + ports: + - "8092:80" """, + "notes": "GCHQ's data transformation tool", + }, + { + "name": "Stirling-PDF", + "desc": "PDF manipulation tools (merge, split, convert, OCR)", + "cat": "Tools", + "image": "frooodle/s-pdf:latest", + "ports": {"8093": "Web UI"}, + "ui_port": 8093, + "compose": """ stirling-pdf: + image: frooodle/s-pdf:latest + container_name: stirling-pdf + restart: always + ports: + - "8093:8080" + volumes: + - /opt/seteclabs/stirling-pdf:/usr/share/tessdata""", + "notes": "All-in-one PDF toolkit", + }, + + # ── Media ── + { + "name": "Jellyfin", + "desc": "Media server (Plex alternative, fully open source)", + "cat": "Media", + "image": "jellyfin/jellyfin:latest", + "ports": {"8096": "Web UI"}, + "ui_port": 8096, + "compose": """ jellyfin: + image: jellyfin/jellyfin:latest + container_name: jellyfin + restart: always + ports: + - "8096:8096" + volumes: + - /opt/seteclabs/jellyfin/config:/config + - /opt/seteclabs/jellyfin/cache:/cache + - /opt/seteclabs/media:/media""", + "notes": "Stream movies, TV, music. Setup wizard on first launch.", + }, + + # ── CI/CD ── + { + "name": "Woodpecker CI", + "desc": "Lightweight CI/CD engine (Drone fork)", + "cat": "CI/CD", + "image": "woodpeckerci/woodpecker-server:latest", + "ports": {"8000": "Web UI"}, + "ui_port": 8000, + "compose": """ woodpecker: + image: woodpeckerci/woodpecker-server:latest + container_name: woodpecker + restart: always + ports: + - "8000:8000" + environment: + - WOODPECKER_HOST=http://localhost:8000 + - WOODPECKER_OPEN=true + - WOODPECKER_GITEA=true + - WOODPECKER_GITEA_URL=https://repo.seteclabs.io + volumes: + - /opt/seteclabs/woodpecker:/data""", + "notes": "Integrates with Gitea. Needs OAuth app setup in Gitea.", + }, + { + "name": "Drone", + "desc": "Container-native CI/CD platform", + "cat": "CI/CD", + "image": "drone/drone:latest", + "ports": {"8001": "Web UI"}, + "ui_port": 8001, + "compose": """ drone: + image: drone/drone:latest + container_name: drone + restart: always + ports: + - "8001:80" + environment: + - DRONE_GITEA_SERVER=https://repo.seteclabs.io + - DRONE_SERVER_HOST=localhost:8001 + - DRONE_SERVER_PROTO=http + volumes: + - /opt/seteclabs/drone:/data""", + "notes": "Needs Gitea OAuth app for authentication", + }, + + # ── Dashboards ── + { + "name": "Homer", + "desc": "Simple static homepage / application dashboard", + "cat": "Dashboards", + "image": "b4bz/homer:latest", + "ports": {"8094": "Web UI"}, + "ui_port": 8094, + "compose": """ homer: + image: b4bz/homer:latest + container_name: homer + restart: always + ports: + - "8094:8080" + volumes: + - /opt/seteclabs/homer:/www/assets""", + "notes": "Edit config.yml in the volume to add your services", + }, + { + "name": "Homarr", + "desc": "Modern dashboard with Docker integration", + "cat": "Dashboards", + "image": "ghcr.io/ajnart/homarr:latest", + "ports": {"7575": "Web UI"}, + "ui_port": 7575, + "compose": """ homarr: + image: ghcr.io/ajnart/homarr:latest + container_name: homarr + restart: always + ports: + - "7575:7575" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /opt/seteclabs/homarr/configs:/app/data/configs + - /opt/seteclabs/homarr/icons:/app/public/icons""", + "notes": "Auto-discovers Docker containers", + }, + + # ── VPN ── + { + "name": "wg-easy", + "desc": "WireGuard VPN with web UI", + "cat": "VPN", + "image": "ghcr.io/wg-easy/wg-easy:latest", + "ports": {"51821": "Web UI", "51820": "WireGuard"}, + "ui_port": 51821, + "compose": """ wg-easy: + image: ghcr.io/wg-easy/wg-easy:latest + container_name: wg-easy + restart: always + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv4.ip_forward=1 + ports: + - "51820:51820/udp" + - "51821:51821/tcp" + environment: + - WG_HOST=31.220.20.55 + - PASSWORD=setecWG2026 + volumes: + - /opt/seteclabs/wg-easy:/etc/wireguard""", + "notes": "Easy WireGuard management. Open UDP 51820 in firewall.", + }, +] + +CATEGORIES = sorted(set(app["cat"] for app in STORE)) diff --git a/setec-web/e2e.py b/setec-web/e2e.py new file mode 100644 index 0000000..c893f70 --- /dev/null +++ b/setec-web/e2e.py @@ -0,0 +1,76 @@ +"""E2E SSH Encryption Protocol — encrypt commands before SSH, decrypt responses after. + +Even if SSH transport is compromised (MITM, key theft, rogue server), +command payloads and responses remain encrypted with the tunnel key. + +Protocol: + Client -> VPS: base64( nonce[12] + AES-256-GCM(tunnel_key, command) ) + VPS -> Client: base64( nonce[12] + AES-256-GCM(tunnel_key, response_json) ) + +The tunnel key is a random 32-byte key shared between client and VPS agent. +The Go agent on the VPS holds the same key at /etc/setec/tunnel.key. +""" + +import os +import json +import base64 +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +import config + +AGENT_PATH = "/usr/local/bin/setec-agent" +KEY_PATH = "/etc/setec/tunnel.key" + + +def encrypt_command(tunnel_key: bytes, command: str) -> str: + """Encrypt a shell command. Returns base64 string.""" + nonce = os.urandom(12) + aesgcm = AESGCM(tunnel_key) + ct = aesgcm.encrypt(nonce, command.encode("utf-8"), None) + return base64.b64encode(nonce + ct).decode("ascii") + + +def decrypt_response(tunnel_key: bytes, b64data: str) -> dict: + """Decrypt an encrypted response from the agent. Returns parsed JSON.""" + raw = base64.b64decode(b64data.strip()) + if len(raw) < 28: # 12 nonce + 16 tag minimum + raise ValueError("Response too short to be valid ciphertext") + nonce = raw[:12] + ct = raw[12:] + aesgcm = AESGCM(tunnel_key) + plaintext = aesgcm.decrypt(nonce, ct, None) + return json.loads(plaintext.decode("utf-8")) + + +def is_e2e_enabled() -> bool: + """Check if E2E encryption is configured and deployed.""" + cfg = config.load() + return cfg.get("e2e_enabled", False) and cfg.get("e2e_tunnel_key_deployed", False) + + +def get_tunnel_key() -> bytes: + """Load the tunnel key from local config (hex-encoded).""" + cfg = config.load() + key_hex = cfg.get("tunnel_key", "") + if not key_hex: + raise RuntimeError("No tunnel key configured. Run E2E setup first.") + return bytes.fromhex(key_hex) + + +def wrap_command_for_agent(tunnel_key: bytes, command: str) -> str: + """Build the full SSH command that pipes encrypted data to the agent.""" + encrypted = encrypt_command(tunnel_key, command) + return f'echo "{encrypted}" | {AGENT_PATH}' + + +def generate_deploy_commands(tunnel_key: bytes) -> list: + """Generate shell commands to deploy the tunnel key on the VPS. + + Returns list of (description, command) tuples. + """ + key_hex = tunnel_key.hex() + commands = [ + ("Create setec directory", "mkdir -p /etc/setec && chmod 700 /etc/setec"), + ("Write tunnel key", f"echo '{key_hex}' > {KEY_PATH} && chmod 600 {KEY_PATH}"), + ("Set key ownership", f"chown root:root {KEY_PATH}"), + ] + return commands diff --git a/setec-web/firewalld.py b/setec-web/firewalld.py new file mode 100644 index 0000000..419bd5f --- /dev/null +++ b/setec-web/firewalld.py @@ -0,0 +1,96 @@ +""" +Command-builder module for managing firewalld on a Linux VPS. +Each function returns a bash command string (or multi-command string). +""" + + +def _perm(permanent: bool) -> str: + return " --permanent" if permanent else "" + + +def status_cmd() -> str: + return ( + "which firewall-cmd > /dev/null 2>&1 && echo 'firewalld is installed' || echo 'firewalld is NOT installed'; " + "firewall-cmd --state 2>/dev/null; " + "systemctl status firewalld --no-pager" + ) + + +def install_cmd() -> str: + return ( + "apt update && apt install -y firewalld && " + "systemctl enable firewalld && " + "systemctl start firewalld" + ) + + +def zones_cmd() -> str: + return "firewall-cmd --get-zones; firewall-cmd --get-active-zones" + + +def zone_info_cmd(zone: str = "public") -> str: + return f"firewall-cmd --zone={zone} --list-all" + + +def add_service_cmd(service: str, zone: str = "public", permanent: bool = True) -> str: + return f"firewall-cmd --zone={zone} --add-service={service}{_perm(permanent)}" + + +def remove_service_cmd(service: str, zone: str = "public", permanent: bool = True) -> str: + return f"firewall-cmd --zone={zone} --remove-service={service}{_perm(permanent)}" + + +def add_port_cmd(port: str, zone: str = "public", permanent: bool = True) -> str: + return f"firewall-cmd --zone={zone} --add-port={port}{_perm(permanent)}" + + +def remove_port_cmd(port: str, zone: str = "public", permanent: bool = True) -> str: + return f"firewall-cmd --zone={zone} --remove-port={port}{_perm(permanent)}" + + +def add_rich_rule_cmd(rule: str, zone: str = "public", permanent: bool = True) -> str: + return f"firewall-cmd --zone={zone} --add-rich-rule='{rule}'{_perm(permanent)}" + + +def remove_rich_rule_cmd(rule: str, zone: str = "public", permanent: bool = True) -> str: + return f"firewall-cmd --zone={zone} --remove-rich-rule='{rule}'{_perm(permanent)}" + + +def block_ip_cmd(ip: str, zone: str = "drop") -> str: + return f"firewall-cmd --zone={zone} --add-source={ip} --permanent" + + +def unblock_ip_cmd(ip: str, zone: str = "drop") -> str: + return f"firewall-cmd --zone={zone} --remove-source={ip} --permanent" + + +def reload_cmd() -> str: + return "firewall-cmd --reload" + + +def panic_on_cmd() -> str: + return "firewall-cmd --panic-on" + + +def panic_off_cmd() -> str: + return "firewall-cmd --panic-off" + + +def log_cmd(lines: int = 50) -> str: + return f"journalctl -u firewalld --no-pager -n {lines}" + + +def services_list_cmd() -> str: + return "firewall-cmd --get-services" + + +def default_zone_cmd(zone: str) -> str: + return f"firewall-cmd --set-default-zone={zone}" + + +def uninstall_cmd() -> str: + return ( + "systemctl stop firewalld && " + "systemctl disable firewalld && " + "apt remove -y firewalld" + ) diff --git a/setec-web/hardening.py b/setec-web/hardening.py new file mode 100644 index 0000000..2da7232 --- /dev/null +++ b/setec-web/hardening.py @@ -0,0 +1,220 @@ +# Security hardening commands for Linux VPS +# Each function returns a bash command string that app.py executes via ssh_run() + + +def ssh_harden_cmd(port=22, disable_root=True, disable_password=True): + """Return bash command to harden SSH config. Backs up sshd_config first.""" + root_login = "no" if disable_root else "yes" + password_auth = "no" if disable_password else "yes" + return ( + "cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak.$(date +%Y%m%d_%H%M%S) && " + f"sed -i 's/^#\\?Port .*/Port {port}/' /etc/ssh/sshd_config && " + f"sed -i 's/^#\\?PermitRootLogin .*/PermitRootLogin {root_login}/' /etc/ssh/sshd_config && " + f"sed -i 's/^#\\?PasswordAuthentication .*/PasswordAuthentication {password_auth}/' /etc/ssh/sshd_config && " + "sed -i 's/^#\\?PubkeyAuthentication .*/PubkeyAuthentication yes/' /etc/ssh/sshd_config && " + "sed -i 's/^#\\?X11Forwarding .*/X11Forwarding no/' /etc/ssh/sshd_config && " + "sed -i 's/^#\\?MaxAuthTries .*/MaxAuthTries 3/' /etc/ssh/sshd_config && " + "sed -i 's/^#\\?AllowAgentForwarding .*/AllowAgentForwarding no/' /etc/ssh/sshd_config && " + "grep -q '^Port ' /etc/ssh/sshd_config || echo 'Port {port}' >> /etc/ssh/sshd_config && ".format(port=port) + + "grep -q '^PermitRootLogin ' /etc/ssh/sshd_config || echo 'PermitRootLogin {root}' >> /etc/ssh/sshd_config && ".format(root=root_login) + + "grep -q '^PasswordAuthentication ' /etc/ssh/sshd_config || echo 'PasswordAuthentication {pw}' >> /etc/ssh/sshd_config && ".format(pw=password_auth) + + "grep -q '^PubkeyAuthentication ' /etc/ssh/sshd_config || echo 'PubkeyAuthentication yes' >> /etc/ssh/sshd_config && " + "sshd -t 2>&1 && " + "systemctl restart sshd 2>&1 && " + "echo 'SSH hardened: Port={port}, RootLogin={root}, PasswordAuth={pw}'".format( + port=port, root=root_login, pw=password_auth + ) + ) + + +def kernel_harden_cmd(): + """Return bash command to apply kernel hardening via sysctl.""" + sysctl_conf = ( + "# Setec Labs kernel hardening\\n" + "net.ipv4.tcp_syncookies = 1\\n" + "net.ipv4.conf.all.rp_filter = 1\\n" + "net.ipv4.icmp_echo_ignore_broadcasts = 1\\n" + "net.ipv4.conf.all.accept_redirects = 0\\n" + "net.ipv4.conf.all.send_redirects = 0\\n" + "net.ipv4.conf.all.accept_source_route = 0\\n" + "net.ipv6.conf.all.accept_redirects = 0\\n" + "kernel.randomize_va_space = 2\\n" + "net.ipv4.tcp_max_syn_backlog = 2048\\n" + "net.ipv4.tcp_synack_retries = 2\\n" + "fs.protected_hardlinks = 1\\n" + "fs.protected_symlinks = 1" + ) + return ( + "cp /etc/sysctl.d/99-hardening.conf /etc/sysctl.d/99-hardening.conf.bak.$(date +%Y%m%d_%H%M%S) 2>/dev/null; " + f"echo -e '{sysctl_conf}' > /etc/sysctl.d/99-hardening.conf && " + "sysctl --system 2>&1 && " + "echo 'Kernel hardening applied via /etc/sysctl.d/99-hardening.conf'" + ) + + +def auto_updates_cmd(): + """Return bash cmd to install and configure unattended-upgrades.""" + return ( + "DEBIAN_FRONTEND=noninteractive apt-get install -y unattended-upgrades apt-listchanges 2>&1 && " + "cat > /etc/apt/apt.conf.d/20auto-upgrades << 'EOF'\n" + 'APT::Periodic::Update-Package-Lists "1";\n' + 'APT::Periodic::Unattended-Upgrade "1";\n' + 'APT::Periodic::AutocleanInterval "7";\n' + "EOF\n" + "cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'EOF'\n" + "Unattended-Upgrade::Allowed-Origins {\n" + ' \"${distro_id}:${distro_codename}\";\n' + ' \"${distro_id}:${distro_codename}-security\";\n' + ' \"${distro_id}ESMApps:${distro_codename}-apps-security\";\n' + ' \"${distro_id}ESM:${distro_codename}-infra-security\";\n' + "};\n" + 'Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";\n' + 'Unattended-Upgrade::Remove-Unused-Dependencies "true";\n' + 'Unattended-Upgrade::Automatic-Reboot "false";\n' + "EOF\n" + "systemctl enable unattended-upgrades 2>&1 && " + "systemctl restart unattended-upgrades 2>&1 && " + "echo 'Unattended upgrades configured and enabled'" + ) + + +def firewall_status_cmd(): + """Return bash cmd to get UFW status verbose.""" + return "ufw status verbose 2>&1" + + +def firewall_enable_cmd(ssh_port=22): + """Return bash cmd to enable UFW with sensible defaults.""" + return ( + "apt-get install -y ufw 2>&1 && " + "ufw default deny incoming 2>&1 && " + "ufw default allow outgoing 2>&1 && " + f"ufw allow {ssh_port}/tcp comment 'SSH' 2>&1 && " + "ufw allow 80/tcp comment 'HTTP' 2>&1 && " + "ufw allow 443/tcp comment 'HTTPS' 2>&1 && " + "echo 'y' | ufw enable 2>&1 && " + "ufw status verbose 2>&1" + ) + + +def firewall_add_rule_cmd(rule): + """Return bash cmd to add a UFW rule (e.g. 'allow 8080/tcp').""" + return f"ufw {rule} 2>&1 && ufw status numbered 2>&1" + + +def firewall_delete_rule_cmd(rule): + """Return bash cmd to delete a UFW rule.""" + return f"echo 'y' | ufw delete {rule} 2>&1 && ufw status numbered 2>&1" + + +def firewall_preset_cmd(preset): + """Return bash cmd for common firewall presets.""" + presets = { + "webserver": ( + "ufw default deny incoming 2>&1 && " + "ufw default allow outgoing 2>&1 && " + "ufw allow 22/tcp comment 'SSH' 2>&1 && " + "ufw allow 80/tcp comment 'HTTP' 2>&1 && " + "ufw allow 443/tcp comment 'HTTPS' 2>&1 && " + "echo 'y' | ufw enable 2>&1 && " + "echo 'Webserver preset applied' && ufw status verbose 2>&1" + ), + "mailserver": ( + "ufw default deny incoming 2>&1 && " + "ufw default allow outgoing 2>&1 && " + "ufw allow 22/tcp comment 'SSH' 2>&1 && " + "ufw allow 25/tcp comment 'SMTP' 2>&1 && " + "ufw allow 80/tcp comment 'HTTP' 2>&1 && " + "ufw allow 443/tcp comment 'HTTPS' 2>&1 && " + "ufw allow 465/tcp comment 'SMTPS' 2>&1 && " + "ufw allow 587/tcp comment 'Submission' 2>&1 && " + "ufw allow 993/tcp comment 'IMAPS' 2>&1 && " + "echo 'y' | ufw enable 2>&1 && " + "echo 'Mailserver preset applied' && ufw status verbose 2>&1" + ), + "lockdown": ( + "ufw default deny incoming 2>&1 && " + "ufw default deny outgoing 2>&1 && " + "ufw allow out 53 comment 'DNS' 2>&1 && " + "ufw allow out 80/tcp comment 'HTTP out' 2>&1 && " + "ufw allow out 443/tcp comment 'HTTPS out' 2>&1 && " + "ufw allow 22/tcp comment 'SSH' 2>&1 && " + "echo 'y' | ufw enable 2>&1 && " + "echo 'Lockdown preset applied - SSH only inbound' && ufw status verbose 2>&1" + ), + } + if preset not in presets: + return f"echo 'Unknown preset: {preset}. Available: webserver, mailserver, lockdown'" + return presets[preset] + + +def user_audit_cmd(): + """Return bash cmd to audit users, SUID binaries, world-writable files, sudoers, recent logins.""" + return ( + "echo '=== USERS WITH SHELLS ===' && " + "grep -v '/nologin\\|/false\\|/sync' /etc/passwd | cut -d: -f1,6,7 && " + "echo '' && echo '=== SUDOERS ===' && " + "getent group sudo 2>/dev/null | cut -d: -f4 && " + "cat /etc/sudoers.d/* 2>/dev/null | grep -v '^#' | grep -v '^$' && " + "echo '' && echo '=== UID 0 ACCOUNTS ===' && " + "awk -F: '$3 == 0 {print $1}' /etc/passwd && " + "echo '' && echo '=== SUID BINARIES ===' && " + "find / -perm -4000 -type f 2>/dev/null | head -30 && " + "echo '' && echo '=== WORLD-WRITABLE FILES (non /proc /sys /dev) ===' && " + "find / -xdev -type f -perm -0002 2>/dev/null | head -20 && " + "echo '' && echo '=== RECENT LOGINS ===' && " + "last -n 15 2>/dev/null && " + "echo '' && echo '=== FAILED LOGIN ATTEMPTS ===' && " + "lastb -n 15 2>/dev/null || journalctl _SYSTEMD_UNIT=sshd.service --no-pager -n 15 2>/dev/null" + ) + + +def port_scan_cmd(): + """Return bash cmd to scan open ports with ss.""" + return ( + "echo '=== LISTENING PORTS ===' && " + "ss -tlnp 2>&1 && " + "echo '' && echo '=== UDP LISTENERS ===' && " + "ss -ulnp 2>&1 && " + "echo '' && echo '=== ESTABLISHED CONNECTIONS ===' && " + "ss -tnp state established 2>&1 | head -30" + ) + + +def ssh_status_cmd(): + """Return bash cmd to show current SSH config settings.""" + return ( + "echo '=== SSHD CONFIG ===' && " + "grep -E '^(Port|PermitRootLogin|PasswordAuthentication|PubkeyAuthentication|" + "X11Forwarding|MaxAuthTries|AllowUsers|AllowGroups|Protocol|LoginGraceTime)' " + "/etc/ssh/sshd_config 2>/dev/null && " + "echo '' && echo '=== SSHD STATUS ===' && " + "systemctl status sshd --no-pager -l 2>&1 | head -15 && " + "echo '' && echo '=== AUTHORIZED KEYS ===' && " + "for u in $(awk -F: '$3>=1000{print $1}' /etc/passwd) root; do " + " f=\"/home/$u/.ssh/authorized_keys\"; " + " [ \"$u\" = 'root' ] && f='/root/.ssh/authorized_keys'; " + " if [ -f \"$f\" ]; then echo \"$u: $(wc -l < \"$f\") keys\"; fi; " + "done" + ) + + +def kernel_status_cmd(): + """Return bash cmd to show current sysctl security settings.""" + return ( + "echo '=== SYSCTL SECURITY SETTINGS ===' && " + "sysctl net.ipv4.tcp_syncookies " + "net.ipv4.conf.all.rp_filter " + "net.ipv4.icmp_echo_ignore_broadcasts " + "net.ipv4.conf.all.accept_redirects " + "net.ipv4.conf.all.send_redirects " + "net.ipv4.conf.all.accept_source_route " + "net.ipv6.conf.all.accept_redirects " + "kernel.randomize_va_space " + "net.ipv4.tcp_max_syn_backlog " + "net.ipv4.tcp_synack_retries " + "fs.protected_hardlinks " + "fs.protected_symlinks 2>&1 && " + "echo '' && echo '=== HARDENING CONFIG FILE ===' && " + "cat /etc/sysctl.d/99-hardening.conf 2>/dev/null || echo 'No hardening config found'" + ) diff --git a/setec-web/hosting.py b/setec-web/hosting.py new file mode 100644 index 0000000..26b75db --- /dev/null +++ b/setec-web/hosting.py @@ -0,0 +1,105 @@ +# Hosting provider API definitions +# Each provider has its DNS API endpoint format and auth method + +PROVIDERS = { + "hostinger": { + "name": "Hostinger", + "dns_base": "https://api.hostinger.com/api/dns/v1/zones", + "auth_type": "bearer", + "auth_header": "Authorization", + "auth_prefix": "Bearer ", + "docs": "https://developers.hostinger.com", + "api_key_label": "API Key", + "notes": "Generate API key from Hostinger hPanel > API Keys", + }, + "cloudflare": { + "name": "Cloudflare", + "dns_base": "https://api.cloudflare.com/client/v4/zones", + "auth_type": "bearer", + "auth_header": "Authorization", + "auth_prefix": "Bearer ", + "docs": "https://developers.cloudflare.com/api", + "api_key_label": "API Token", + "notes": "Create API token at dash.cloudflare.com > My Profile > API Tokens", + "needs_zone_id": True, + }, + "digitalocean": { + "name": "DigitalOcean", + "dns_base": "https://api.digitalocean.com/v2/domains", + "auth_type": "bearer", + "auth_header": "Authorization", + "auth_prefix": "Bearer ", + "docs": "https://docs.digitalocean.com/reference/api", + "api_key_label": "API Token", + "notes": "Generate token at cloud.digitalocean.com > API > Tokens", + }, + "vultr": { + "name": "Vultr", + "dns_base": "https://api.vultr.com/v2/domains", + "auth_type": "bearer", + "auth_header": "Authorization", + "auth_prefix": "Bearer ", + "docs": "https://www.vultr.com/api", + "api_key_label": "API Key", + "notes": "Get API key from my.vultr.com > Account > API", + }, + "linode": { + "name": "Linode (Akamai)", + "dns_base": "https://api.linode.com/v4/domains", + "auth_type": "bearer", + "auth_header": "Authorization", + "auth_prefix": "Bearer ", + "docs": "https://www.linode.com/docs/api", + "api_key_label": "Personal Access Token", + "notes": "Create token at cloud.linode.com > My Profile > API Tokens", + }, + "godaddy": { + "name": "GoDaddy", + "dns_base": "https://api.godaddy.com/v1/domains", + "auth_type": "sso", + "auth_header": "Authorization", + "auth_prefix": "sso-key ", + "docs": "https://developer.godaddy.com", + "api_key_label": "API Key:Secret", + "notes": "Format: key:secret. Get from developer.godaddy.com > API Keys", + }, + "namecheap": { + "name": "Namecheap", + "dns_base": "https://api.namecheap.com/xml.response", + "auth_type": "query", + "docs": "https://www.namecheap.com/support/api/intro", + "api_key_label": "API Key", + "notes": "Enable API at namecheap.com > Profile > Tools > API Access. Requires IP whitelist.", + }, + "hetzner": { + "name": "Hetzner", + "dns_base": "https://dns.hetzner.com/api/v1/zones", + "auth_type": "header", + "auth_header": "Auth-API-Token", + "auth_prefix": "", + "docs": "https://dns.hetzner.com/api-docs", + "api_key_label": "API Token", + "notes": "Create token at dns.hetzner.com > API Tokens", + }, + "ovh": { + "name": "OVH", + "dns_base": "https://api.ovh.com/1.0/domain/zone", + "auth_type": "ovh", + "docs": "https://api.ovh.com", + "api_key_label": "Application Key", + "notes": "Requires Application Key, Application Secret, and Consumer Key from api.ovh.com/createApp", + }, + "aws_route53": { + "name": "AWS Route 53", + "dns_base": "https://route53.amazonaws.com/2013-04-01/hostedzone", + "auth_type": "aws", + "docs": "https://docs.aws.amazon.com/Route53/latest/APIReference", + "api_key_label": "Access Key ID", + "notes": "Requires AWS Access Key ID and Secret Access Key with Route53 permissions", + }, +} + +PROVIDER_LIST = [ + {"id": k, **{kk: vv for kk, vv in v.items()}} + for k, v in PROVIDERS.items() +] diff --git a/setec-web/iptables.py b/setec-web/iptables.py new file mode 100644 index 0000000..dee3ec7 --- /dev/null +++ b/setec-web/iptables.py @@ -0,0 +1,209 @@ +# iptables firewall management commands +# Each function returns a bash command string that app.py executes via ssh_run() + +import re + +BUILTIN_CHAINS = {"INPUT", "OUTPUT", "FORWARD", "PREROUTING", "POSTROUTING"} +POLICY_TARGETS = {"ACCEPT", "DROP", "REJECT"} + + +def _validate_chain(chain): + """Validate chain name: built-in or alphanumeric custom chain.""" + chain = chain.strip() + if chain.upper() in BUILTIN_CHAINS: + return chain.upper() + if re.match(r'^[A-Za-z0-9_-]+$', chain) and len(chain) <= 30: + return chain + raise ValueError(f"Invalid chain name: {chain}") + + +def _validate_ip(ip): + """Validate an IPv4 or IPv6 address (no CIDR for block/unblock).""" + ip = ip.strip() + # IPv4 + if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', ip): + parts = ip.split('.') + if all(0 <= int(p) <= 255 for p in parts): + return ip + # IPv4 CIDR + if re.match(r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$', ip): + addr, prefix = ip.rsplit('/', 1) + parts = addr.split('.') + if all(0 <= int(p) <= 255 for p in parts) and 0 <= int(prefix) <= 32: + return ip + # IPv6 (basic check) + if re.match(r'^[0-9a-fA-F:]+$', ip) or re.match(r'^[0-9a-fA-F:]+/\d{1,3}$', ip): + return ip + raise ValueError(f"Invalid IP address: {ip}") + + +def status_cmd(): + """Return bash cmd to check if iptables is installed and show version.""" + return ( + "echo '=== iptables Installation ===' && " + "which iptables >/dev/null 2>&1 && iptables -V 2>&1 || echo 'iptables not installed' && " + "echo '' && echo '=== ip6tables ===' && " + "which ip6tables >/dev/null 2>&1 && ip6tables -V 2>&1 || echo 'ip6tables not installed' && " + "echo '' && echo '=== iptables-persistent ===' && " + "dpkg -l | grep iptables-persistent | awk '{print $2, $3}' 2>/dev/null || echo 'iptables-persistent not installed'" + ) + + +def list_cmd(): + """Return bash cmd to list all iptables rules with line numbers.""" + return ( + "echo '=== iptables -L (filter) ===' && " + "iptables -L -v -n --line-numbers 2>&1" + ) + + +def list_nat_cmd(): + """Return bash cmd to list NAT table rules.""" + return ( + "echo '=== iptables -t nat ===' && " + "iptables -t nat -L -v -n --line-numbers 2>&1" + ) + + +def list_mangle_cmd(): + """Return bash cmd to list mangle table rules.""" + return ( + "echo '=== iptables -t mangle ===' && " + "iptables -t mangle -L -v -n 2>&1" + ) + + +def add_rule_cmd(chain, rule): + """Return bash cmd to append a rule to a chain.""" + chain = _validate_chain(chain) + return f"iptables -A {chain} {rule} 2>&1 && echo 'Rule appended to {chain}'" + + +def insert_rule_cmd(chain, position, rule): + """Return bash cmd to insert a rule at a position in a chain.""" + chain = _validate_chain(chain) + position = int(position) + if position < 1: + raise ValueError(f"Invalid position: {position}") + return f"iptables -I {chain} {position} {rule} 2>&1 && echo 'Rule inserted in {chain} at position {position}'" + + +def delete_rule_cmd(chain, rule_num): + """Return bash cmd to delete a rule by number from a chain.""" + chain = _validate_chain(chain) + rule_num = int(rule_num) + if rule_num < 1: + raise ValueError(f"Invalid rule number: {rule_num}") + return f"iptables -D {chain} {rule_num} 2>&1 && echo 'Rule {rule_num} deleted from {chain}'" + + +def flush_cmd(chain=None): + """Return bash cmd to flush all rules or a specific chain.""" + if chain is not None: + chain = _validate_chain(chain) + return f"iptables -F {chain} 2>&1 && echo 'Flushed chain {chain}'" + return "iptables -F 2>&1 && echo 'All chains flushed'" + + +def save_cmd(): + """Return bash cmd to save current rules to /etc/iptables/rules.v4.""" + return ( + "if ! dpkg -l | grep -q iptables-persistent; then " + " DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + " DEBIAN_FRONTEND=noninteractive apt-get install -y iptables-persistent 2>&1; " + "fi && " + "mkdir -p /etc/iptables && " + "iptables-save > /etc/iptables/rules.v4 2>&1 && " + "echo 'Rules saved to /etc/iptables/rules.v4'" + ) + + +def restore_cmd(): + """Return bash cmd to restore rules from /etc/iptables/rules.v4.""" + return ( + "if [ -f /etc/iptables/rules.v4 ]; then " + " iptables-restore < /etc/iptables/rules.v4 2>&1 && " + " echo 'Rules restored from /etc/iptables/rules.v4'; " + "else " + " echo 'No saved rules found at /etc/iptables/rules.v4'; " + "fi" + ) + + +def policy_cmd(chain, target): + """Return bash cmd to set default policy for a chain.""" + chain = _validate_chain(chain) + target = target.strip().upper() + if target not in POLICY_TARGETS: + raise ValueError(f"Invalid policy target: {target} (must be ACCEPT, DROP, or REJECT)") + if chain not in {"INPUT", "OUTPUT", "FORWARD"}: + raise ValueError(f"Policy can only be set on INPUT, OUTPUT, or FORWARD (got {chain})") + return f"iptables -P {chain} {target} 2>&1 && echo 'Policy for {chain} set to {target}'" + + +def block_ip_cmd(ip): + """Return bash cmd to block an IP on INPUT and FORWARD chains.""" + ip = _validate_ip(ip) + return ( + f"iptables -A INPUT -s {ip} -j DROP 2>&1 && " + f"iptables -A FORWARD -s {ip} -j DROP 2>&1 && " + f"echo 'Blocked {ip} on INPUT and FORWARD'" + ) + + +def unblock_ip_cmd(ip): + """Return bash cmd to remove all block rules for an IP.""" + ip = _validate_ip(ip) + return ( + f"while iptables -D INPUT -s {ip} -j DROP 2>/dev/null; do :; done && " + f"while iptables -D FORWARD -s {ip} -j DROP 2>/dev/null; do :; done && " + f"while iptables -D INPUT -s {ip} -j REJECT 2>/dev/null; do :; done && " + f"while iptables -D FORWARD -s {ip} -j REJECT 2>/dev/null; do :; done && " + f"echo 'Unblocked {ip} from INPUT and FORWARD'" + ) + + +def list_blocked_cmd(): + """Return bash cmd to show all DROP and REJECT rules.""" + return ( + "echo '=== Blocked (DROP/REJECT) Rules ===' && " + "iptables -L -v -n --line-numbers 2>&1 | grep -E 'DROP|REJECT' || echo 'No blocked rules found'" + ) + + +def log_cmd(lines=50): + """Return bash cmd to grep iptables entries from kernel/syslog.""" + lines = int(lines) + return ( + "echo '=== iptables Log Entries ===' && " + f"if [ -f /var/log/kern.log ]; then " + f" grep -i 'iptables\\|netfilter\\|\\[UFW' /var/log/kern.log 2>/dev/null | tail -{lines}; " + f"elif [ -f /var/log/syslog ]; then " + f" grep -i 'iptables\\|netfilter\\|\\[UFW' /var/log/syslog 2>/dev/null | tail -{lines}; " + f"elif [ -f /var/log/messages ]; then " + f" grep -i 'iptables\\|netfilter' /var/log/messages 2>/dev/null | tail -{lines}; " + f"else " + f" dmesg | grep -i 'iptables\\|netfilter' 2>/dev/null | tail -{lines} || echo 'No iptables log entries found'; " + f"fi" + ) + + +def ip6_list_cmd(): + """Return bash cmd to list all ip6tables rules with line numbers.""" + return ( + "echo '=== ip6tables -L (filter) ===' && " + "ip6tables -L -v -n --line-numbers 2>&1" + ) + + +def counters_cmd(): + """Return bash cmd to show packet and byte counters per rule.""" + return ( + "echo '=== Packet/Byte Counters ===' && " + "iptables -L -v -n -x 2>&1" + ) + + +def zero_counters_cmd(): + """Return bash cmd to zero all packet and byte counters.""" + return "iptables -Z 2>&1 && echo 'All counters zeroed'" diff --git a/setec-web/lynis.py b/setec-web/lynis.py new file mode 100644 index 0000000..640c2e7 --- /dev/null +++ b/setec-web/lynis.py @@ -0,0 +1,98 @@ +""" +Command-builder module for managing Lynis (security auditing tool) on a Linux VPS. +Each function returns a bash command string. +""" + + +def status_cmd(): + """Check if Lynis is installed, show version and update info.""" + return ( + "echo '=== Lynis Status ===' && " + "if command -v lynis >/dev/null 2>&1; then " + "echo 'Installed: yes' && lynis --version && echo '--- Update Info ---' && lynis update info; " + "else echo 'Installed: no'; fi" + ) + + +def install_cmd(): + """Install Lynis via apt.""" + return "apt-get update && apt-get install -y lynis" + + +def audit_full_cmd(): + """Run a full Lynis system audit with no colors, capturing full output.""" + return "lynis audit system --no-colors" + + +def audit_quick_cmd(): + """Run a quick Lynis system audit with no colors, show last 80 lines.""" + return "lynis audit system --quick --no-colors | tail -n 80" + + +def show_report_cmd(): + """Cat the Lynis report and parse key findings.""" + return ( + "echo '=== Lynis Report Key Findings ===' && " + "echo '--- Warnings ---' && " + "grep -E '^warning\\[\\]=' /var/log/lynis-report.dat 2>/dev/null | sed 's/warning\\[\\]=//' || echo 'No warnings found' && " + "echo '--- Suggestions ---' && " + "grep -E '^suggestion\\[\\]=' /var/log/lynis-report.dat 2>/dev/null | head -20 | sed 's/suggestion\\[\\]=//' || echo 'No suggestions found' && " + "echo '--- Hardening Index ---' && " + "grep -E '^hardening_index=' /var/log/lynis-report.dat 2>/dev/null | sed 's/hardening_index=/Score: /' || echo 'No hardening index found'" + ) + + +def show_warnings_cmd(): + """Grep warnings from the Lynis report.""" + return "grep -E '^warning\\[\\]=' /var/log/lynis-report.dat 2>/dev/null | sed 's/warning\\[\\]=//' || echo 'No warnings found'" + + +def show_suggestions_cmd(): + """Grep suggestions from the Lynis report.""" + return "grep -E '^suggestion\\[\\]=' /var/log/lynis-report.dat 2>/dev/null | sed 's/suggestion\\[\\]=//' || echo 'No suggestions found'" + + +def hardening_index_cmd(): + """Extract the hardening index score from the Lynis report.""" + return "grep -E '^hardening_index=' /var/log/lynis-report.dat 2>/dev/null | sed 's/hardening_index=/Hardening Index: /' || echo 'No hardening index found'" + + +def log_cmd(lines=100): + """View the last N lines of the Lynis log.""" + return f"tail -n {lines} /var/log/lynis.log" + + +def profile_cmd(): + """Show the default Lynis audit profile.""" + return "cat /etc/lynis/default.prf" + + +def schedule_cmd(schedule="weekly"): + """Create a cron job for scheduled Lynis audits.""" + cron_schedules = { + "daily": "0 3 * * *", + "weekly": "0 3 * * 0", + "monthly": "0 3 1 * *", + } + cron_time = cron_schedules.get(schedule, cron_schedules["weekly"]) + cron_line = f"{cron_time} root lynis audit system --no-colors --quick > /var/log/lynis-scheduled.log 2>&1" + return ( + f"echo '{cron_line}' > /etc/cron.d/lynis-audit && " + "chmod 644 /etc/cron.d/lynis-audit && " + f"echo 'Lynis {schedule} audit scheduled'" + ) + + +def schedule_status_cmd(): + """Check if a scheduled Lynis audit cron job exists.""" + return "cat /etc/cron.d/lynis-audit 2>/dev/null || echo 'No scheduled Lynis audit found'" + + +def schedule_remove_cmd(): + """Remove the scheduled Lynis audit cron job.""" + return "rm -f /etc/cron.d/lynis-audit && echo 'Lynis scheduled audit removed'" + + +def uninstall_cmd(): + """Uninstall Lynis via apt.""" + return "apt-get remove -y lynis && apt-get autoremove -y" diff --git a/setec-web/modsecurity.py b/setec-web/modsecurity.py new file mode 100644 index 0000000..6c21987 --- /dev/null +++ b/setec-web/modsecurity.py @@ -0,0 +1,188 @@ +""" +Command-builder module for managing ModSecurity WAF with OWASP CRS on Nginx. +Each function returns a bash command string for execution on a Linux VPS. +""" + +MODSEC_CONF = "/etc/nginx/modsec/modsecurity.conf" +CRS_SETUP_CONF = "/etc/nginx/modsec/owasp-crs/crs-setup.conf" +CRS_RULES_DIR = "/etc/nginx/modsec/owasp-crs/rules" +MAIN_CONF = "/etc/nginx/modsec/main.conf" +EXCLUSIONS_CONF = "/etc/nginx/modsec/exclusions.conf" +AUDIT_LOG = "/var/log/modsec_audit.log" +DEBUG_LOG = "/var/log/modsecurity/modsec_debug.log" +CRS_DIR = "/etc/nginx/modsec/owasp-crs" + + +def status_cmd(): + """Check if modsecurity module is loaded in nginx, rules active, and SecRuleEngine setting.""" + return ( + "echo '=== Nginx ModSecurity Module ===' && " + "nginx -V 2>&1 | grep -i modsecurity && " + "echo '' && " + "echo '=== SecRuleEngine Setting ===' && " + f"grep -i 'SecRuleEngine' {MODSEC_CONF} 2>/dev/null || echo 'modsecurity.conf not found' && " + "echo '' && " + "echo '=== Active Rules ===' && " + f"ls {CRS_RULES_DIR}/*.conf 2>/dev/null | wc -l && " + "echo 'rule files loaded' && " + "echo '' && " + "echo '=== Nginx Config Test ===' && " + "nginx -t 2>&1" + ) + + +def install_cmd(): + """Install libmodsecurity3, nginx module, download OWASP CRS, configure.""" + return ( + "apt-get update && " + "apt-get install -y libmodsecurity3 libmodsecurity-dev libnginx-mod-http-modsecurity && " + "mkdir -p /etc/nginx/modsec && " + f"cd /etc/nginx/modsec && " + "if [ ! -d owasp-crs ]; then " + "git clone https://github.com/coreruleset/coreruleset.git owasp-crs; " + "fi && " + f"cp {CRS_DIR}/crs-setup.conf.example {CRS_SETUP_CONF} && " + "cp /etc/modsecurity/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf && " + f"sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' {MODSEC_CONF} && " + f"touch {EXCLUSIONS_CONF} && " + f"cat > {MAIN_CONF} << 'MAINEOF'\n" + f"Include {MODSEC_CONF}\n" + f"Include {CRS_SETUP_CONF}\n" + f"Include {CRS_RULES_DIR}/*.conf\n" + f"Include {EXCLUSIONS_CONF}\n" + "MAINEOF\n" + "&& nginx -t && systemctl reload nginx" + ) + + +def enable_cmd(): + """Set SecRuleEngine On in modsecurity.conf.""" + return ( + f"sed -i 's/^SecRuleEngine .*/SecRuleEngine On/' {MODSEC_CONF} && " + f"grep 'SecRuleEngine' {MODSEC_CONF} && " + "nginx -t && systemctl reload nginx" + ) + + +def disable_cmd(): + """Set SecRuleEngine to DetectionOnly (log only, don't block).""" + return ( + f"sed -i 's/^SecRuleEngine .*/SecRuleEngine DetectionOnly/' {MODSEC_CONF} && " + f"grep 'SecRuleEngine' {MODSEC_CONF} && " + "nginx -t && systemctl reload nginx" + ) + + +def audit_log_cmd(lines=50): + """Tail the modsecurity audit log.""" + return f"tail -n {int(lines)} {AUDIT_LOG}" + + +def debug_log_cmd(lines=50): + """Tail the modsecurity debug log.""" + return f"tail -n {int(lines)} {DEBUG_LOG}" + + +def rules_list_cmd(): + """List rule files in the OWASP CRS rules directory.""" + return f"ls -la {CRS_RULES_DIR}/*.conf 2>/dev/null" + + +def rule_disable_cmd(rule_id): + """Add SecRuleRemoveById to the custom exclusion conf.""" + rid = str(int(rule_id)) + return ( + f"if grep -q 'SecRuleRemoveById {rid}' {EXCLUSIONS_CONF} 2>/dev/null; then " + f"echo 'Rule {rid} is already disabled'; " + "else " + f"echo 'SecRuleRemoveById {rid}' >> {EXCLUSIONS_CONF} && " + f"echo 'Disabled rule {rid}' && " + "nginx -t && systemctl reload nginx; " + "fi" + ) + + +def rule_enable_cmd(rule_id): + """Remove a rule ID from the exclusion conf (re-enable it).""" + rid = str(int(rule_id)) + return ( + f"if grep -q 'SecRuleRemoveById {rid}' {EXCLUSIONS_CONF} 2>/dev/null; then " + f"sed -i '/SecRuleRemoveById {rid}/d' {EXCLUSIONS_CONF} && " + f"echo 'Re-enabled rule {rid}' && " + "nginx -t && systemctl reload nginx; " + "else " + f"echo 'Rule {rid} is not in exclusions'; " + "fi" + ) + + +def exclusions_cmd(): + """Show current rule exclusions.""" + return ( + f"echo '=== Rule Exclusions ({EXCLUSIONS_CONF}) ===' && " + f"cat {EXCLUSIONS_CONF} 2>/dev/null || echo 'No exclusions file found'" + ) + + +def crs_update_cmd(): + """Git pull in the OWASP CRS directory to update rules.""" + return ( + f"cd {CRS_DIR} && " + "git pull && " + "echo '' && echo '=== Updated CRS Rules ===' && " + "nginx -t && systemctl reload nginx" + ) + + +def config_cmd(): + """Show modsecurity.conf.""" + return f"cat {MODSEC_CONF}" + + +def config_crs_cmd(): + """Show crs-setup.conf.""" + return f"cat {CRS_SETUP_CONF}" + + +def test_cmd(): + """Curl localhost with test XSS and SQLi payloads to verify WAF is working.""" + return ( + "echo '=== XSS Test ===' && " + "curl -s -o /dev/null -w 'HTTP %{http_code}' " + "'http://localhost/?q=' && " + "echo '' && " + "echo '=== SQLi Test ===' && " + "curl -s -o /dev/null -w 'HTTP %{http_code}' " + "\"http://localhost/?id=1' OR '1'='1\" && " + "echo '' && " + "echo '=== Path Traversal Test ===' && " + "curl -s -o /dev/null -w 'HTTP %{http_code}' " + "'http://localhost/../../etc/passwd' && " + "echo '' && " + "echo '(403 = blocked by WAF, 200 = WAF not blocking)'" + ) + + +def nginx_status_cmd(): + """Check which nginx server blocks have modsecurity enabled.""" + return ( + "echo '=== Server Blocks with ModSecurity ===' && " + "grep -rn 'modsecurity' /etc/nginx/sites-enabled/ /etc/nginx/conf.d/ 2>/dev/null || " + "echo 'No modsecurity directives found in server blocks' && " + "echo '' && " + "echo '=== Nginx Module Status ===' && " + "nginx -V 2>&1 | grep -io 'modsecurity[^ ]*' || echo 'ModSecurity module not found'" + ) + + +def uninstall_cmd(): + """Remove modsec configs and apt remove packages.""" + return ( + "echo 'Removing ModSecurity configuration...' && " + "rm -rf /etc/nginx/modsec && " + "apt-get remove -y libmodsecurity3 libmodsecurity-dev libnginx-mod-http-modsecurity && " + "apt-get autoremove -y && " + "echo 'Removed modsec configs and packages' && " + "echo 'NOTE: Check nginx server blocks and remove modsecurity directives manually' && " + "nginx -t && systemctl reload nginx" + ) diff --git a/setec-web/monitoring.py b/setec-web/monitoring.py new file mode 100644 index 0000000..40cbcd0 --- /dev/null +++ b/setec-web/monitoring.py @@ -0,0 +1,359 @@ +# IDS, log monitoring, login tracking, file integrity, process auditing, and alerting +# Each function returns a bash command string that app.py executes via ssh_run() + + +def auth_log_cmd(lines=100): + """Return bash cmd to get recent auth log entries (failed/successful logins).""" + return ( + "echo '=== Recent Auth Log Entries ===' && " + "if [ -f /var/log/auth.log ]; then " + f" grep -E '(Failed|Accepted|Invalid|session opened|session closed|authentication failure)' /var/log/auth.log | tail -n {lines}; " + "elif command -v journalctl >/dev/null 2>&1; then " + f" journalctl -u sshd -u ssh --no-pager -n {lines} 2>&1; " + "else " + " echo 'No auth.log found and journalctl not available'; " + "fi" + ) + + +def login_tracker_cmd(): + """Return bash cmd to show login attempts with IP, count, and geo info.""" + return ( + "echo '=== Login Tracker: Top 20 IPs ===' && " + "echo '' && " + "echo '--- Failed Logins ---' && " + "if [ -f /var/log/auth.log ]; then " + " FAILED_IPS=$(grep -oP 'Failed password.*from \\K[0-9.]+' /var/log/auth.log 2>/dev/null | " + " sort | uniq -c | sort -rn | head -20); " + "else " + " FAILED_IPS=$(journalctl -u sshd -u ssh --no-pager 2>/dev/null | " + " grep -oP 'Failed password.*from \\K[0-9.]+' | " + " sort | uniq -c | sort -rn | head -20); " + "fi && " + "if [ -z \"$FAILED_IPS\" ]; then " + " echo 'No failed login attempts found'; " + "else " + " echo \"$FAILED_IPS\" | while read COUNT IP; do " + " GEO=''; " + " if command -v geoiplookup >/dev/null 2>&1; then " + " GEO=$(geoiplookup \"$IP\" 2>/dev/null | head -1 | sed 's/GeoIP Country Edition: //'); " + " elif command -v whois >/dev/null 2>&1; then " + " GEO=$(whois \"$IP\" 2>/dev/null | grep -i -m1 'country' | awk '{print $NF}'); " + " fi; " + " printf '%6s %-16s %s\\n' \"$COUNT\" \"$IP\" \"$GEO\"; " + " done; " + "fi && " + "echo '' && " + "echo '--- Accepted Logins ---' && " + "if [ -f /var/log/auth.log ]; then " + " ACCEPT_IPS=$(grep -oP 'Accepted.*from \\K[0-9.]+' /var/log/auth.log 2>/dev/null | " + " sort | uniq -c | sort -rn | head -20); " + "else " + " ACCEPT_IPS=$(journalctl -u sshd -u ssh --no-pager 2>/dev/null | " + " grep -oP 'Accepted.*from \\K[0-9.]+' | " + " sort | uniq -c | sort -rn | head -20); " + "fi && " + "if [ -z \"$ACCEPT_IPS\" ]; then " + " echo 'No accepted logins found'; " + "else " + " echo \"$ACCEPT_IPS\" | while read COUNT IP; do " + " GEO=''; " + " if command -v geoiplookup >/dev/null 2>&1; then " + " GEO=$(geoiplookup \"$IP\" 2>/dev/null | head -1 | sed 's/GeoIP Country Edition: //'); " + " elif command -v whois >/dev/null 2>&1; then " + " GEO=$(whois \"$IP\" 2>/dev/null | grep -i -m1 'country' | awk '{print $NF}'); " + " fi; " + " printf '%6s %-16s %s\\n' \"$COUNT\" \"$IP\" \"$GEO\"; " + " done; " + "fi" + ) + + +def active_sessions_cmd(): + """Return bash cmd to show who is currently logged in.""" + return ( + "echo '=== Currently Logged In ===' && " + "w 2>&1 && " + "echo '' && " + "echo '=== Active Sessions (who) ===' && " + "who 2>&1 && " + "echo '' && " + "echo '=== Recent Logins (last 10) ===' && " + "last -10 2>&1" + ) + + +def file_integrity_check_cmd(paths="/etc /usr/bin /usr/sbin /var/www"): + """Return bash cmd to create/check file integrity baseline.""" + db = "/var/lib/setec-integrity.db" + return ( + f"if [ -f {db} ]; then " + f" echo '=== File Integrity Check (comparing to baseline) ===' && " + f" CURRENT=$(mktemp) && " + f" find {paths} -type f -exec md5sum {{}} + 2>/dev/null | sort -k2 > \"$CURRENT\" && " + f" BASELINE=$(sort -k2 {db}) && " + " echo '' && " + " echo '--- Modified Files ---' && " + f" diff <(echo \"$BASELINE\") \"$CURRENT\" 2>/dev/null | grep '^[<>]' | head -50 && " + " echo '' && " + " echo '--- New Files (not in baseline) ---' && " + f" comm -13 <(awk '{{print $2}}' {db} | sort) <(awk '{{print $2}}' \"$CURRENT\" | sort) | head -50 && " + " echo '' && " + " echo '--- Deleted Files (in baseline but missing) ---' && " + f" comm -23 <(awk '{{print $2}}' {db} | sort) <(awk '{{print $2}}' \"$CURRENT\" | sort) | head -50 && " + " MODIFIED=$(diff <(echo \"$BASELINE\") \"$CURRENT\" 2>/dev/null | grep -c '^[<>]' || true) && " + " echo '' && " + " echo \"Summary: $MODIFIED differences found\" && " + " rm -f \"$CURRENT\"; " + "else " + f" echo 'No baseline found at {db}. Creating initial baseline...' && " + f" find {paths} -type f -exec md5sum {{}} + 2>/dev/null | sort -k2 > {db} && " + f" COUNT=$(wc -l < {db}) && " + " echo \"Baseline created with $COUNT files\"; " + "fi" + ) + + +def file_integrity_init_cmd(paths="/etc /usr/bin /usr/sbin /var/www"): + """Return bash cmd to initialize/reset the integrity baseline.""" + db = "/var/lib/setec-integrity.db" + return ( + f"echo '=== Initializing File Integrity Baseline ===' && " + f"mkdir -p /var/lib && " + f"find {paths} -type f -exec md5sum {{}} + 2>/dev/null | sort -k2 > {db} && " + f"COUNT=$(wc -l < {db}) && " + f"echo \"Baseline created at {db} with $COUNT files\" && " + f"echo \"Monitored paths: {paths}\"" + ) + + +def process_audit_cmd(): + """Return bash cmd to find suspicious processes.""" + return ( + "echo '=== Process Security Audit ===' && " + "echo '' && " + "echo '--- Listening Ports with Processes ---' && " + "ss -tlnp 2>&1 && " + "echo '' && " + "echo '--- UDP Listening Ports ---' && " + "ss -ulnp 2>&1 && " + "echo '' && " + "echo '--- Processes Running as Root (non-kernel) ---' && " + "ps aux --sort=-%mem 2>/dev/null | awk '$1==\"root\" && $11!~/^\\[/' | head -30 && " + "echo '' && " + "echo '--- Processes with Deleted Binaries (suspicious) ---' && " + "ls -la /proc/*/exe 2>/dev/null | grep '(deleted)' || echo 'None found' && " + "echo '' && " + "echo '--- Recently Modified Binaries (last 7 days) ---' && " + "find /usr/bin /usr/sbin /usr/local/bin -type f -mtime -7 -ls 2>/dev/null | head -30 || echo 'None found' && " + "echo '' && " + "echo '--- Unusual SUID/SGID Binaries ---' && " + "find /usr/bin /usr/sbin /usr/local/bin /tmp /var/tmp -type f \\( -perm -4000 -o -perm -2000 \\) -ls 2>/dev/null | head -30 || echo 'None found'" + ) + + +def security_log_cmd(lines=100): + """Return bash cmd to get combined security-relevant logs.""" + return ( + "echo '=== Combined Security Logs ===' && " + "( " + " [ -f /var/log/auth.log ] && grep -E '(Failed|Accepted|Invalid|authentication failure|sudo|su:)' /var/log/auth.log 2>/dev/null; " + " [ -f /var/log/fail2ban.log ] && tail -50 /var/log/fail2ban.log 2>/dev/null; " + " [ -f /var/log/kern.log ] && grep -iE '(iptables|firewall|segfault|oom)' /var/log/kern.log 2>/dev/null; " + " [ -f /var/log/syslog ] && grep -iE '(security|attack|denied|unauthorized|blocked)' /var/log/syslog 2>/dev/null; " + " journalctl -p warning --since '24 hours ago' --no-pager 2>/dev/null | head -50; " + f") | sort -t' ' -k1,3 2>/dev/null | tail -n {lines}" + ) + + +def alert_setup_cmd(email, webhook_url=""): + """Return bash cmd to set up alert script for suspicious activity.""" + webhook_section = "" + if webhook_url: + webhook_section = ( + f" if command -v curl >/dev/null 2>&1; then\\n" + f" curl -s -X POST -H 'Content-Type: application/json' " + f"-d \\\"{{\\\\\\\"text\\\\\\\": \\\\\\\"$MSG\\\\\\\"}}\\\" " + f"'{webhook_url}' >/dev/null 2>&1\\n" + f" fi\\n" + ) + + script = ( + "#!/bin/bash\\n" + "# Setec Labs Security Alert Script\\n" + "LOGFILE=/var/log/setec-alerts.log\\n" + "STATEFILE=/var/lib/setec-alert-state\\n" + "THRESHOLD=10\\n" + "\\n" + "mkdir -p /var/lib\\n" + "touch \\\"$STATEFILE\\\" \\\"$LOGFILE\\\"\\n" + "\\n" + "LAST_CHECK=$(cat \\\"$STATEFILE\\\" 2>/dev/null || echo 0)\\n" + "NOW=$(date +%s)\\n" + "echo \\\"$NOW\\\" > \\\"$STATEFILE\\\"\\n" + "\\n" + "# Check failed SSH logins since last check\\n" + "if [ -f /var/log/auth.log ]; then\\n" + " FAILED=$(grep 'Failed password' /var/log/auth.log 2>/dev/null | wc -l)\\n" + "else\\n" + " FAILED=$(journalctl -u sshd --since '5 minutes ago' --no-pager 2>/dev/null | grep -c 'Failed password')\\n" + "fi\\n" + "\\n" + "# Check fail2ban bans\\n" + "BANS=0\\n" + "if [ -f /var/log/fail2ban.log ]; then\\n" + " BANS=$(grep -c 'Ban' /var/log/fail2ban.log 2>/dev/null || echo 0)\\n" + "fi\\n" + "\\n" + "# Check for new SUID files\\n" + "NEWSUID=$(find /tmp /var/tmp /home -type f -perm -4000 2>/dev/null | wc -l)\\n" + "\\n" + "ALERT=0\\n" + "MSG=\\\"\\\"\\n" + "\\n" + "if [ \\\"$FAILED\\\" -gt \\\"$THRESHOLD\\\" ]; then\\n" + " MSG=\\\"ALERT: $FAILED failed SSH login attempts detected\\\"\\n" + " ALERT=1\\n" + "fi\\n" + "\\n" + "if [ \\\"$NEWSUID\\\" -gt 0 ]; then\\n" + " MSG=\\\"$MSG\\\\nALERT: $NEWSUID SUID files found in /tmp, /var/tmp, or /home\\\"\\n" + " ALERT=1\\n" + "fi\\n" + "\\n" + "if [ \\\"$ALERT\\\" -eq 1 ]; then\\n" + " HOSTNAME=$(hostname)\\n" + " MSG=\\\"[$HOSTNAME] $(date): $MSG\\\"\\n" + " echo \\\"$MSG\\\" >> \\\"$LOGFILE\\\"\\n" + f" if command -v mail >/dev/null 2>&1; then\\n" + f" echo -e \\\"$MSG\\\" | mail -s \\\"Setec Security Alert: $HOSTNAME\\\" {email} 2>/dev/null\\n" + f" fi\\n" + f"{webhook_section}" + "fi\\n" + ) + + return ( + "echo '=== Setting Up Security Alerting ===' && " + f"echo -e '{script}' > /usr/local/bin/setec-alert.sh && " + "chmod +x /usr/local/bin/setec-alert.sh && " + "touch /var/log/setec-alerts.log && " + "(crontab -l 2>/dev/null | grep -v 'setec-alert.sh'; " + "echo '*/5 * * * * /usr/local/bin/setec-alert.sh') | crontab - && " + f"echo 'Alert script installed at /usr/local/bin/setec-alert.sh' && " + f"echo 'Cron job added: runs every 5 minutes' && " + f"echo 'Alert email: {email}'" + ) + + +def alert_status_cmd(): + """Return bash cmd to check if alerting is configured and show recent alerts.""" + return ( + "echo '=== Alert System Status ===' && " + "echo '' && " + "echo '--- Script ---' && " + "if [ -f /usr/local/bin/setec-alert.sh ]; then " + " echo 'Alert script: INSTALLED'; " + " ls -la /usr/local/bin/setec-alert.sh; " + "else " + " echo 'Alert script: NOT INSTALLED'; " + "fi && " + "echo '' && " + "echo '--- Cron Job ---' && " + "if crontab -l 2>/dev/null | grep -q 'setec-alert.sh'; then " + " echo 'Cron job: ACTIVE'; " + " crontab -l 2>/dev/null | grep 'setec-alert.sh'; " + "else " + " echo 'Cron job: NOT FOUND'; " + "fi && " + "echo '' && " + "echo '--- Recent Alerts (last 20) ---' && " + "if [ -f /var/log/setec-alerts.log ]; then " + " tail -20 /var/log/setec-alerts.log; " + "else " + " echo 'No alert log found'; " + "fi" + ) + + +def alert_remove_cmd(): + """Return bash cmd to remove alerting.""" + return ( + "echo '=== Removing Security Alerting ===' && " + "(crontab -l 2>/dev/null | grep -v 'setec-alert.sh') | crontab - && " + "rm -f /usr/local/bin/setec-alert.sh && " + "echo 'Alert script removed' && " + "echo 'Cron job removed' && " + "echo 'Alert log preserved at /var/log/setec-alerts.log'" + ) + + +def suid_audit_cmd(): + """Return bash cmd to find all SUID/SGID binaries.""" + return ( + "echo '=== SUID/SGID Binary Audit ===' && " + "echo '' && " + "echo '--- SUID Binaries ---' && " + "find / -type f -perm -4000 -not -path '/proc/*' -not -path '/sys/*' -ls 2>/dev/null && " + "echo '' && " + "echo '--- SGID Binaries ---' && " + "find / -type f -perm -2000 -not -path '/proc/*' -not -path '/sys/*' -ls 2>/dev/null && " + "echo '' && " + "SUID_COUNT=$(find / -type f -perm -4000 -not -path '/proc/*' -not -path '/sys/*' 2>/dev/null | wc -l) && " + "SGID_COUNT=$(find / -type f -perm -2000 -not -path '/proc/*' -not -path '/sys/*' 2>/dev/null | wc -l) && " + "echo \"Total: $SUID_COUNT SUID, $SGID_COUNT SGID binaries\"" + ) + + +def world_writable_cmd(): + """Return bash cmd to find world-writable files/dirs.""" + return ( + "echo '=== World-Writable Files ===' && " + "find / -type f -perm -0002 " + "-not -path '/proc/*' -not -path '/sys/*' -not -path '/dev/*' " + "-ls 2>/dev/null | head -50 && " + "echo '' && " + "echo '=== World-Writable Directories (without sticky bit) ===' && " + "find / -type d -perm -0002 -not -perm -1000 " + "-not -path '/proc/*' -not -path '/sys/*' -not -path '/dev/*' " + "-ls 2>/dev/null | head -50" + ) + + +def cron_audit_cmd(): + """Return bash cmd to audit all cron jobs on the system.""" + return ( + "echo '=== Cron Job Audit ===' && " + "echo '' && " + "echo '--- System Crontab (/etc/crontab) ---' && " + "cat /etc/crontab 2>/dev/null || echo 'Not found' && " + "echo '' && " + "echo '--- /etc/cron.d/ ---' && " + "for f in /etc/cron.d/*; do " + " [ -f \"$f\" ] && echo \"== $f ==\" && cat \"$f\" && echo ''; " + "done 2>/dev/null && " + "echo '' && " + "echo '--- /etc/cron.daily/ ---' && " + "ls -la /etc/cron.daily/ 2>/dev/null || echo 'Not found' && " + "echo '' && " + "echo '--- /etc/cron.hourly/ ---' && " + "ls -la /etc/cron.hourly/ 2>/dev/null || echo 'Not found' && " + "echo '' && " + "echo '--- /etc/cron.weekly/ ---' && " + "ls -la /etc/cron.weekly/ 2>/dev/null || echo 'Not found' && " + "echo '' && " + "echo '--- /etc/cron.monthly/ ---' && " + "ls -la /etc/cron.monthly/ 2>/dev/null || echo 'Not found' && " + "echo '' && " + "echo '--- User Crontabs ---' && " + "for user in $(cut -f1 -d: /etc/passwd 2>/dev/null); do " + " CRON=$(crontab -u \"$user\" -l 2>/dev/null); " + " if [ -n \"$CRON\" ]; then " + " echo \"== $user ==\"; " + " echo \"$CRON\"; " + " echo ''; " + " fi; " + "done && " + "echo '' && " + "echo '--- Systemd Timers ---' && " + "systemctl list-timers --all --no-pager 2>/dev/null || echo 'systemctl not available'" + ) diff --git a/setec-web/nftables.py b/setec-web/nftables.py new file mode 100644 index 0000000..cbcab74 --- /dev/null +++ b/setec-web/nftables.py @@ -0,0 +1,110 @@ +""" +Command-builder module for managing nftables on a Linux VPS. +Each function returns a bash command string ready for execution. +""" + + +def status_cmd() -> str: + """Check if nft is installed, its version, and the systemctl status of nftables.""" + return ( + "which nft && nft --version; " + "systemctl status nftables --no-pager" + ) + + +def install_cmd() -> str: + """Install nftables and enable the service.""" + return ( + "apt-get update && apt-get install -y nftables && " + "systemctl enable nftables && systemctl start nftables" + ) + + +def list_cmd() -> str: + """List the full nftables ruleset.""" + return "nft list ruleset" + + +def list_tables_cmd() -> str: + """List all nftables tables.""" + return "nft list tables" + + +def list_chains_cmd(table: str = "inet filter") -> str: + """List all chains in the given table.""" + return f"nft list chains {table}" + + +def add_rule_cmd(table: str, chain: str, rule: str) -> str: + """Add a rule to a chain in a table. + + Example: + add_rule_cmd("inet filter", "input", "tcp dport 80 accept") + """ + return f"nft add rule {table} {chain} {rule}" + + +def delete_rule_cmd(table: str, chain: str, handle: int) -> str: + """Delete a rule by handle number.""" + return f"nft delete rule {table} {chain} handle {handle}" + + +def flush_cmd(table: str | None = None, chain: str | None = None) -> str: + """Flush rules. Optionally scope to a table or table+chain.""" + if table and chain: + return f"nft flush chain {table} {chain}" + if table: + return f"nft flush table {table}" + return "nft flush ruleset" + + +def create_table_cmd(family: str, name: str) -> str: + """Create a new table (e.g. family='inet', name='filter').""" + return f"nft add table {family} {name}" + + +def delete_table_cmd(family: str, name: str) -> str: + """Delete a table.""" + return f"nft delete table {family} {name}" + + +def create_chain_cmd( + table: str, + chain: str, + chain_type: str = "filter", + hook: str = "input", + priority: int = 0, +) -> str: + """Create a base chain with type, hook, and priority.""" + return ( + f"nft add chain {table} {chain} " + f"'{{ type {chain_type} hook {hook} priority {priority}; }}'" + ) + + +def save_cmd() -> str: + """Save the current ruleset to /etc/nftables.conf.""" + return "nft list ruleset > /etc/nftables.conf" + + +def restore_cmd() -> str: + """Restore rules from /etc/nftables.conf.""" + return "nft -f /etc/nftables.conf" + + +def counters_cmd() -> str: + """List all nftables counters.""" + return "nft list counters" + + +def config_cmd() -> str: + """Display the saved nftables configuration file.""" + return "cat /etc/nftables.conf" + + +def uninstall_cmd() -> str: + """Stop, disable, and remove nftables.""" + return ( + "systemctl stop nftables; systemctl disable nftables; " + "apt-get purge -y nftables && apt-get autoremove -y" + ) diff --git a/setec-web/ossec.py b/setec-web/ossec.py new file mode 100644 index 0000000..4a3f7f0 --- /dev/null +++ b/setec-web/ossec.py @@ -0,0 +1,113 @@ +""" +Command-builder module for managing OSSEC HIDS on a Linux VPS. +Each function returns a bash command string. OSSEC installs to /var/ossec. +""" + + +def status_cmd(): + return ( + "echo '=== OSSEC Status ===' && " + "/var/ossec/bin/ossec-control status && " + "echo && echo '=== OSSEC Version ===' && " + "/var/ossec/bin/ossec-control info 2>/dev/null || " + "cat /var/ossec/etc/ossec-init.conf 2>/dev/null || echo 'Version unknown' && " + "echo && echo '=== Active Processes ===' && " + "ps aux | grep '[o]ssec'" + ) + + +def install_cmd(): + return ( + "apt-get update && " + "apt-get install -y build-essential make gcc libevent-dev libpcre2-dev libz-dev libssl-dev && " + "cd /tmp && " + "wget -O ossec-hids-3.7.0.tar.gz https://github.com/ossec/ossec-hids/archive/refs/tags/3.7.0.tar.gz && " + "tar xzf ossec-hids-3.7.0.tar.gz && " + "cd ossec-hids-3.7.0 && " + "OSSEC_LANGUAGE=en OSSEC_TYPE=local OSSEC_NOTIFY=n OSSEC_SYSCHECK=y " + "OSSEC_ROOTCHECK=y OSSEC_ACTIVE_RESPONSE=y ./install.sh && " + "/var/ossec/bin/ossec-control start && " + "echo 'OSSEC 3.7.0 installed and started.'" + ) + + +def start_cmd(): + return "/var/ossec/bin/ossec-control start" + + +def stop_cmd(): + return "/var/ossec/bin/ossec-control stop" + + +def restart_cmd(): + return "/var/ossec/bin/ossec-control restart" + + +def alerts_cmd(lines=50): + return f"tail -n {lines} /var/ossec/logs/alerts/alerts.log" + + +def alerts_today_cmd(): + return ( + "grep \"$(date +'%Y %b %d')\" /var/ossec/logs/alerts/alerts.log || " + "echo 'No alerts for today.'" + ) + + +def log_cmd(lines=50): + return f"tail -n {lines} /var/ossec/logs/ossec.log" + + +def syscheck_cmd(): + return ( + "echo '=== Syscheck Results ===' && " + "ls -la /var/ossec/queue/syscheck/ && " + "echo && echo '=== Recent Integrity Changes ===' && " + "for f in /var/ossec/queue/syscheck/*; do " + "echo \"--- $f ---\" && tail -20 \"$f\" 2>/dev/null; done" + ) + + +def config_cmd(): + return "cat /var/ossec/etc/ossec.conf" + + +def config_save_cmd(content): + escaped = content.replace("'", "'\\''") + return ( + "cp /var/ossec/etc/ossec.conf /var/ossec/etc/ossec.conf.bak.$(date +%Y%m%d%H%M%S) && " + f"echo '{escaped}' > /var/ossec/etc/ossec.conf && " + "/var/ossec/bin/ossec-control restart && " + "echo 'Config saved and OSSEC restarted.'" + ) + + +def rules_cmd(): + return "ls -la /var/ossec/rules/*.xml" + + +def active_response_cmd(): + return ( + "echo '=== Active Response Config ===' && " + "grep -A5 '' /var/ossec/etc/ossec.conf && " + "echo && echo '=== Recent Blocks ===' && " + "cat /var/ossec/logs/active-responses.log 2>/dev/null | tail -30 || " + "echo 'No active response log found.'" + ) + + +def agent_list_cmd(): + return "/var/ossec/bin/agent_control -l" + + +def uninstall_cmd(): + return ( + "/var/ossec/bin/ossec-control stop 2>/dev/null; " + "rm -rf /var/ossec && " + "userdel ossec 2>/dev/null; " + "userdel ossecm 2>/dev/null; " + "userdel ossecr 2>/dev/null; " + "userdel ossece 2>/dev/null; " + "groupdel ossec 2>/dev/null; " + "echo 'OSSEC uninstalled.'" + ) diff --git a/setec-web/requirements.txt b/setec-web/requirements.txt new file mode 100644 index 0000000..67c3105 --- /dev/null +++ b/setec-web/requirements.txt @@ -0,0 +1,4 @@ +flask>=3.0,<4.0 +paramiko>=3.4,<4.0 +waitress>=3.0,<4.0 +requests>=2.31,<3.0 diff --git a/setec-web/rkhunter.py b/setec-web/rkhunter.py new file mode 100644 index 0000000..db7a03f --- /dev/null +++ b/setec-web/rkhunter.py @@ -0,0 +1,152 @@ +# rkhunter rootkit detection management commands +# Each function returns a bash command string that app.py executes via ssh_run() + + +def status_cmd(): + """Return bash cmd to check rkhunter install, version, and last run results.""" + return ( + "echo '=== rkhunter Installation ===' && " + "dpkg -l | grep rkhunter | awk '{print $2, $3}' 2>/dev/null || echo 'rkhunter not installed' && " + "echo '' && echo '=== Version ===' && " + "rkhunter --version 2>/dev/null || echo 'rkhunter not found' && " + "echo '' && echo '=== Last Run ===' && " + "if [ -f /var/log/rkhunter.log ]; then " + " grep 'Start date' /var/log/rkhunter.log | tail -1; " + " grep 'End date' /var/log/rkhunter.log | tail -1; " + " echo '' && echo '=== Last Results ===' && " + " grep -E '\\[Warning\\]|\\[Bad\\]|\\[Not found\\]' /var/log/rkhunter.log | tail -20 || " + " echo 'No warnings found in last run'; " + "else " + " echo 'No log file found (rkhunter has not been run)'; " + "fi" + ) + + +def install_cmd(): + """Return bash cmd to install rkhunter, update db, and set file properties.""" + return ( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y rkhunter 2>&1 && " + "rkhunter --update 2>&1; " + "rkhunter --propupd 2>&1 && " + "echo 'rkhunter installed, database updated, file properties set'" + ) + + +def update_cmd(): + """Return bash cmd to update rkhunter signatures and file properties.""" + return ( + "echo '=== Updating Signatures ===' && " + "rkhunter --update 2>&1; " + "echo '' && echo '=== Updating File Properties ===' && " + "rkhunter --propupd 2>&1 && " + "echo '' && echo 'rkhunter signatures and file properties updated'" + ) + + +def check_cmd(): + """Return bash cmd for a full rkhunter scan (warnings only).""" + return ( + "echo '=== rkhunter Full Scan ===' && " + "echo 'Started: '$(date) && " + "rkhunter --check --skip-keypress --report-warnings-only 2>&1; " + "echo 'Finished: '$(date)" + ) + + +def check_quick_cmd(): + """Return bash cmd for a quick rkhunter check on key areas.""" + return ( + "echo '=== rkhunter Quick Check ===' && " + "echo 'Started: '$(date) && " + "rkhunter --check --skip-keypress --report-warnings-only " + "--enable system_commands,rootkits,network 2>&1; " + "echo 'Finished: '$(date)" + ) + + +def log_cmd(lines=50): + """Return bash cmd to view rkhunter log.""" + return ( + "echo '=== rkhunter Log ===' && " + f"tail -{lines} /var/log/rkhunter.log 2>/dev/null || echo 'No rkhunter log found'" + ) + + +def config_cmd(): + """Return bash cmd to show rkhunter config and key settings.""" + return ( + "echo '=== rkhunter.conf ===' && " + "cat /etc/rkhunter.conf 2>/dev/null || echo 'Not found' && " + "echo '' && echo '=== Key Settings ===' && " + "grep -E '^(ALLOW_SSH_ROOT_USER|ALLOW_SSH_PROT_V1|ENABLE_TESTS|DISABLE_TESTS|" + "SCRIPTWHITELIST|ALLOWHIDDENDIR|ALLOWHIDDENFILE|ALLOWDEVFILE|" + "WEB_CMD|UPDATE_MIRRORS|MIRRORS_MODE|MAIL-ON-WARNING)' " + "/etc/rkhunter.conf 2>/dev/null || echo 'Could not read config'" + ) + + +def whitelist_cmd(): + """Return bash cmd to show current whitelisted items from config.""" + return ( + "echo '=== rkhunter Whitelisted Items ===' && " + "grep -E '^(SCRIPTWHITELIST|ALLOWHIDDENDIR|ALLOWHIDDENFILE|ALLOWDEVFILE|" + "ALLOW_SSH_ROOT_USER|RTKT_FILE_WHITELIST|RTKT_DIR_WHITELIST|" + "SHARED_LIB_WHITELIST|PORT_WHITELIST|EXISTWHITELIST)' " + "/etc/rkhunter.conf 2>/dev/null || echo 'No whitelist entries found or config not found'" + ) + + +def whitelist_add_cmd(item): + """Return bash cmd to add a SCRIPTWHITELIST entry to rkhunter.conf.""" + return ( + f"if grep -q '^SCRIPTWHITELIST={item}$' /etc/rkhunter.conf 2>/dev/null; then " + f" echo 'Already whitelisted: {item}'; " + f"else " + f" echo 'SCRIPTWHITELIST={item}' >> /etc/rkhunter.conf && " + f" echo 'Added SCRIPTWHITELIST={item} to /etc/rkhunter.conf' && " + f" rkhunter --propupd 2>&1; " + f"fi" + ) + + +def schedule_cmd(schedule="daily"): + """Return bash cmd to set up a cron job for scheduled rkhunter scanning.""" + if schedule == "daily": + cron_time = "0 4 * * *" + elif schedule == "weekly": + cron_time = "0 4 * * 0" + else: + cron_time = "0 4 * * *" + return ( + f"(crontab -l 2>/dev/null | grep -v 'setec-rkhunter'; " + f"echo '{cron_time} rkhunter --check --skip-keypress --report-warnings-only " + f"--logfile /var/log/rkhunter.log # setec-rkhunter') | crontab - 2>&1 && " + f"echo 'Scheduled {schedule} rkhunter scan' && " + f"crontab -l | grep setec-rkhunter" + ) + + +def schedule_status_cmd(): + """Return bash cmd to show current rkhunter scan schedule.""" + return ( + "echo '=== rkhunter Scan Schedule ===' && " + "crontab -l 2>/dev/null | grep setec-rkhunter || echo 'No scheduled rkhunter scan'" + ) + + +def schedule_remove_cmd(): + """Return bash cmd to remove scheduled rkhunter scan.""" + return ( + "(crontab -l 2>/dev/null | grep -v 'setec-rkhunter') | crontab - 2>&1 && " + "echo 'Scheduled rkhunter scan removed'" + ) + + +def uninstall_cmd(): + """Return bash cmd to remove rkhunter.""" + return ( + "DEBIAN_FRONTEND=noninteractive apt-get remove --purge -y rkhunter 2>&1 && " + "apt-get autoremove -y 2>&1 && " + "echo 'rkhunter uninstalled'" + ) diff --git a/setec-web/run.bat b/setec-web/run.bat new file mode 100644 index 0000000..9e5f8a7 --- /dev/null +++ b/setec-web/run.bat @@ -0,0 +1,13 @@ +@echo off +cd /d "%~dp0" +echo =================================== +echo SETEC LABS Manager v2.0 +echo http://localhost:5000 +echo =================================== +echo. + +pip install flask paramiko requests >nul 2>&1 + +echo Starting server... +python app.py +pause diff --git a/setec-web/sanitize.py b/setec-web/sanitize.py new file mode 100644 index 0000000..fe86413 --- /dev/null +++ b/setec-web/sanitize.py @@ -0,0 +1,101 @@ +"""Input sanitization — prevent command injection in SSH commands.""" + +import re + + +def hostname(val): + """Validate and sanitize a hostname/domain.""" + val = str(val).strip().lower() + if not re.match(r'^[a-z0-9]([a-z0-9\-\.]{0,253}[a-z0-9])?$', val): + raise ValueError(f"Invalid hostname: {val}") + return val + + +def ip_address(val): + """Validate an IPv4 or IPv6 address.""" + val = str(val).strip() + # IPv4 + if re.match(r'^(\d{1,3}\.){3}\d{1,3}$', val): + parts = val.split(".") + if all(0 <= int(p) <= 255 for p in parts): + return val + # IPv6 + if re.match(r'^[0-9a-fA-F:]+$', val) and "::" in val or val.count(":") >= 2: + return val + # CIDR + if re.match(r'^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$', val): + return val + raise ValueError(f"Invalid IP address: {val}") + + +def port(val): + """Validate a port number.""" + val = int(val) + if not 1 <= val <= 65535: + raise ValueError(f"Invalid port: {val}") + return val + + +def filepath(val, allow_absolute=True): + """Sanitize a file path — block shell metacharacters and traversal.""" + val = str(val).strip() + # Block shell metacharacters + dangerous = set(';&|`$(){}[]!#~<>\\"\'\n\r\t') + if any(c in val for c in dangerous): + raise ValueError(f"Path contains forbidden characters: {val}") + # Block path traversal + if ".." in val: + raise ValueError(f"Path traversal not allowed: {val}") + if not allow_absolute and val.startswith("/"): + raise ValueError(f"Absolute paths not allowed: {val}") + return val + + +def shell_arg(val): + """Sanitize a value for safe use in a shell command. + Only allows alphanumeric, dash, underscore, dot, slash, colon, @, space.""" + val = str(val).strip() + if not re.match(r'^[a-zA-Z0-9\-_\./:@ ]+$', val): + raise ValueError(f"Invalid characters in argument: {val}") + return val + + +def service_name(val): + """Validate a systemd service name.""" + val = str(val).strip() + if not re.match(r'^[a-zA-Z0-9\-_@\.]+$', val): + raise ValueError(f"Invalid service name: {val}") + return val + + +def container_name(val): + """Validate a Docker container name.""" + val = str(val).strip() + if not re.match(r'^[a-zA-Z0-9][a-zA-Z0-9_\.\-]+$', val): + raise ValueError(f"Invalid container name: {val}") + return val + + +def dns_record_type(val): + """Validate a DNS record type.""" + val = str(val).strip().upper() + allowed = {"A", "AAAA", "CNAME", "TXT", "MX", "NS", "SRV", "CAA", "PTR"} + if val not in allowed: + raise ValueError(f"Invalid DNS record type: {val}") + return val + + +def email_address(val): + """Basic email validation.""" + val = str(val).strip() + if not re.match(r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$', val): + raise ValueError(f"Invalid email: {val}") + return val + + +def positive_int(val, max_val=10000): + """Validate a positive integer within bounds.""" + val = int(val) + if val < 1 or val > max_val: + raise ValueError(f"Value out of range (1-{max_val}): {val}") + return val diff --git a/setec-web/sec_updates.py b/setec-web/sec_updates.py new file mode 100644 index 0000000..82aaa6a --- /dev/null +++ b/setec-web/sec_updates.py @@ -0,0 +1,141 @@ +# Security update management via .sec files +# Each function returns a bash command string that app.py executes via ssh_run() + +SEC_DIR = "/var/www/updates.seteclabs.io/sec" + + +def os_detect_cmd(): + """Return bash cmd to detect OS distro and version.""" + return ( + "echo '=== OS Detection ===' && " + "if [ -f /etc/os-release ]; then " + " . /etc/os-release && " + " echo \"DISTRO_ID=$ID\" && " + " echo \"DISTRO_VERSION=$VERSION_ID\" && " + " echo \"DISTRO_CODENAME=$VERSION_CODENAME\" && " + " echo \"DISTRO_NAME=$PRETTY_NAME\"; " + "else " + " echo 'ERROR: /etc/os-release not found'; " + "fi && " + "echo \"KERNEL=$(uname -r)\" && " + "echo \"ARCH=$(dpkg --print-architecture 2>/dev/null || uname -m)\"" + ) + + +def list_available_cmd(): + """Return bash cmd to list all available .sec files on the update server.""" + return ( + f"echo '=== Available Security Updates ===' && " + f"ls -1 {SEC_DIR}/*.sec 2>/dev/null | while read f; do " + f" name=$(basename \"$f\"); " + f" desc=$(head -5 \"$f\" | grep '^# Setec Labs' | sed 's/^# //'); " + f" target=$(head -5 \"$f\" | grep '^# Target:' | sed 's/^# Target: //'); " + f" echo \"$name | $target | $desc\"; " + f"done || echo 'No .sec files found in {SEC_DIR}'" + ) + + +def check_updates_cmd(distro_id, version_id): + """Return bash cmd to find .sec files matching a distro+version prefix.""" + # Construct prefix: e.g. ubuntu + 22.04 -> ubuntu2204_ + ver_clean = version_id.replace(".", "") + prefix = f"{distro_id}{ver_clean}_" + return ( + f"echo '=== Updates for {distro_id} {version_id} ===' && " + f"ls -1 {SEC_DIR}/{prefix}*.sec 2>/dev/null | while read f; do " + f" name=$(basename \"$f\"); " + f" echo \"$name\"; " + f"done || echo 'No updates found for prefix: {prefix}'" + ) + + +def download_update_cmd(filename): + """Return bash cmd to show the contents of a .sec file (it's already local on VPS).""" + if ".." in filename or "/" in filename: + return "echo 'ERROR: invalid filename'" + return f"cat {SEC_DIR}/{filename} 2>&1" + + +def preview_update_cmd(filename): + """Return bash cmd to preview what a .sec file will do without applying it.""" + if ".." in filename or "/" in filename: + return "echo 'ERROR: invalid filename'" + path = f"{SEC_DIR}/{filename}" + return ( + f"echo '=== Preview: {filename} ===' && " + f"if [ ! -f '{path}' ]; then echo 'File not found: {filename}'; exit 1; fi && " + f"echo '' && echo '--- [packages] ---' && " + f"sed -n '/^\\[packages\\]/,/^\\[/{{ /^\\[/!p }}' '{path}' | grep -v '^#' | grep -v '^$' && " + f"echo '' && echo '--- [sysctl] ---' && " + f"sed -n '/^\\[sysctl\\]/,/^\\[/{{ /^\\[/!p }}' '{path}' | grep -v '^#' | grep -v '^$' && " + f"echo '' && echo '--- [services] ---' && " + f"sed -n '/^\\[services\\]/,/^\\[/{{ /^\\[/!p }}' '{path}' | grep -v '^#' | grep -v '^$' && " + f"echo '' && echo '--- [files] ---' && " + f"sed -n '/^\\[files\\]/,/^\\[/{{ /^\\[/!p }}' '{path}' | grep -v '^#' | grep -v '^$' && " + f"echo '' && echo '--- [custom] ---' && " + f"sed -n '/^\\[custom\\]/,/^\\[/{{ /^\\[/!p }}' '{path}' | grep -v '^#' | grep -v '^$' && " + f"echo '' && echo '=== End Preview ==='" + ) + + +def parse_and_apply_cmd(filename): + """Return bash cmd to parse a .sec file and apply all sections.""" + if ".." in filename or "/" in filename: + return "echo 'ERROR: invalid filename'" + path = f"{SEC_DIR}/{filename}" + return ( + f"if [ ! -f '{path}' ]; then echo 'File not found: {filename}'; exit 1; fi && " + f"echo '=== Applying: {filename} ===' && " + f"echo \"Started: $(date)\" && " + f"echo '' && " + # Log the apply + f"echo \"$(date '+%Y-%m-%d %H:%M:%S') APPLY {filename}\" >> /var/log/setec-updates.log && " + # [packages] section — run each line as a command + f"echo '>>> [packages]' && " + f"sed -n '/^\\[packages\\]/,/^\\[/{{ /^\\[/!p }}' '{path}' | grep -v '^#' | grep -v '^$' | " + f"while IFS= read -r cmd; do " + f" echo \"+ $cmd\" && " + f" eval \"DEBIAN_FRONTEND=noninteractive $cmd\" 2>&1; " + f"done && " + # [sysctl] section — write to conf file, then apply + f"echo '' && echo '>>> [sysctl]' && " + f"sed -n '/^\\[sysctl\\]/,/^\\[/{{ /^\\[/!p }}' '{path}' | grep -v '^#' | grep -v '^$' > /etc/sysctl.d/99-setec-update.conf && " + f"sysctl -p /etc/sysctl.d/99-setec-update.conf 2>&1 && " + # [services] section — run each line + f"echo '' && echo '>>> [services]' && " + f"sed -n '/^\\[services\\]/,/^\\[/{{ /^\\[/!p }}' '{path}' | grep -v '^#' | grep -v '^$' | " + f"while IFS= read -r cmd; do " + f" echo \"+ $cmd\" && " + f" eval \"$cmd\" 2>&1; " + f"done && " + # [files] section — run each line + f"echo '' && echo '>>> [files]' && " + f"sed -n '/^\\[files\\]/,/^\\[/{{ /^\\[/!p }}' '{path}' | grep -v '^#' | grep -v '^$' | " + f"while IFS= read -r cmd; do " + f" echo \"+ $cmd\" && " + f" eval \"$cmd\" 2>&1; " + f"done && " + # [custom] section — strip bash:-: prefix, run each line + f"echo '' && echo '>>> [custom]' && " + f"sed -n '/^\\[custom\\]/,/^\\[/{{ /^\\[/!p }}' '{path}' | grep -v '^#' | grep -v '^$' | " + f"sed 's/^bash:-://' | " + f"while IFS= read -r cmd; do " + f" echo \"+ $cmd\" && " + f" eval \"$cmd\" 2>&1; " + f"done && " + f"echo '' && echo \"Completed: $(date)\" && " + f"echo \"$(date '+%Y-%m-%d %H:%M:%S') DONE {filename}\" >> /var/log/setec-updates.log && " + f"echo '=== Update Applied Successfully ==='" + ) + + +def update_history_cmd(): + """Return bash cmd to show the update application history.""" + return ( + "echo '=== Setec Update History ===' && " + "if [ -f /var/log/setec-updates.log ]; then " + " cat /var/log/setec-updates.log; " + "else " + " echo 'No update history found'; " + "fi" + ) diff --git a/setec-web/security_apps.py b/setec-web/security_apps.py new file mode 100644 index 0000000..6a32afc --- /dev/null +++ b/setec-web/security_apps.py @@ -0,0 +1,242 @@ +# Security tool definitions for the Setec Manager +# Each entry provides check/install/scan/uninstall command strings +# that app.py executes via ssh_run() + +SECURITY_APPS = [ + { + "name": "ClamAV", + "desc": "Open-source antivirus engine for detecting trojans, viruses, malware", + "cat": "antivirus", + "check": "clamdscan --version 2>/dev/null", + "install": ( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y clamav clamav-daemon 2>&1 && " + "systemctl stop clamav-freshclam 2>/dev/null; " + "freshclam 2>&1 && " + "systemctl enable clamav-freshclam 2>&1 && " + "systemctl start clamav-freshclam 2>&1 && " + "systemctl enable clamav-daemon 2>&1 && " + "systemctl start clamav-daemon 2>&1 && " + "echo 'ClamAV installed and running'" + ), + "scan": "clamscan -r --bell -i /var/www /home /tmp 2>&1 | tail -30", + "uninstall": ( + "systemctl stop clamav-daemon clamav-freshclam 2>/dev/null; " + "systemctl disable clamav-daemon clamav-freshclam 2>/dev/null; " + "apt-get remove -y clamav clamav-daemon clamav-freshclam 2>&1 && " + "apt-get autoremove -y 2>&1 && " + "echo 'ClamAV removed'" + ), + }, + { + "name": "rkhunter", + "desc": "Rootkit detection tool - scans for rootkits, backdoors, and local exploits", + "cat": "rootkit", + "check": "rkhunter --version 2>/dev/null", + "install": ( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y rkhunter 2>&1 && " + "rkhunter --update 2>&1 && " + "rkhunter --propupd 2>&1 && " + "echo 'rkhunter installed and database initialized'" + ), + "scan": "rkhunter --check --skip-keypress --report-warnings-only 2>&1", + "uninstall": ( + "apt-get remove -y rkhunter 2>&1 && " + "apt-get autoremove -y 2>&1 && " + "echo 'rkhunter removed'" + ), + }, + { + "name": "chkrootkit", + "desc": "Another rootkit detection tool - checks for signs of rootkits on the system", + "cat": "rootkit", + "check": "chkrootkit -V 2>/dev/null", + "install": ( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y chkrootkit 2>&1 && " + "echo 'chkrootkit installed'" + ), + "scan": "chkrootkit 2>&1 | grep -v 'not found' | grep -v 'nothing found' | tail -40", + "uninstall": ( + "apt-get remove -y chkrootkit 2>&1 && " + "apt-get autoremove -y 2>&1 && " + "echo 'chkrootkit removed'" + ), + }, + { + "name": "Lynis", + "desc": "Security auditing tool - comprehensive system hardening scanner", + "cat": "audit", + "check": "lynis --version 2>/dev/null", + "install": ( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y lynis 2>&1 && " + "echo 'Lynis installed'" + ), + "scan": "lynis audit system --quick --no-colors 2>&1 | tail -80", + "uninstall": ( + "apt-get remove -y lynis 2>&1 && " + "apt-get autoremove -y 2>&1 && " + "echo 'Lynis removed'" + ), + }, + { + "name": "OSSEC", + "desc": "Host-based intrusion detection system (HIDS) - log analysis, integrity checking, rootkit detection", + "cat": "ids", + "check": "/var/ossec/bin/ossec-control status 2>/dev/null", + "install": ( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y build-essential make gcc libevent-dev zlib1g-dev " + "libssl-dev libpcre2-dev wget 2>&1 && " + "cd /tmp && " + "wget -q https://github.com/ossec/ossec-hids/archive/refs/tags/3.7.0.tar.gz -O ossec-3.7.0.tar.gz 2>&1 && " + "tar xzf ossec-3.7.0.tar.gz 2>&1 && " + "cd ossec-hids-3.7.0 && " + "echo -e '\\nlocal\\n\\n/var/log/syslog\\n/var/log/auth.log\\n\\n\\n\\n\\n' " + "| ./install.sh 2>&1 && " + "/var/ossec/bin/ossec-control start 2>&1 && " + "rm -rf /tmp/ossec-3.7.0.tar.gz /tmp/ossec-hids-3.7.0 && " + "echo 'OSSEC HIDS installed in local mode and started'" + ), + "scan": ( + "/var/ossec/bin/ossec-control status 2>&1 && " + "echo '' && echo '=== RECENT ALERTS ===' && " + "tail -30 /var/ossec/logs/alerts/alerts.log 2>/dev/null || echo 'No alerts yet'" + ), + "uninstall": ( + "/var/ossec/bin/ossec-control stop 2>/dev/null; " + "rm -rf /var/ossec 2>&1 && " + "userdel ossec 2>/dev/null; userdel ossecm 2>/dev/null; userdel ossecr 2>/dev/null; " + "groupdel ossec 2>/dev/null; " + "echo 'OSSEC removed'" + ), + }, + { + "name": "ModSecurity", + "desc": "Web Application Firewall (WAF) for Nginx - OWASP Core Rule Set", + "cat": "waf", + "check": "nginx -V 2>&1 | grep -i modsecurity", + "install": ( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y libmodsecurity3 libmodsecurity-dev " + "nginx-module-modsecurity 2>&1 || " + "DEBIAN_FRONTEND=noninteractive apt-get install -y libmodsecurity3 2>&1 && " + "mkdir -p /etc/nginx/modsec && " + "cp /etc/modsecurity/modsecurity.conf-recommended /etc/nginx/modsec/modsecurity.conf 2>/dev/null || " + "wget -q https://raw.githubusercontent.com/owasp-modsecurity/ModSecurity/v3/master/modsecurity.conf-recommended " + "-O /etc/nginx/modsec/modsecurity.conf 2>&1 && " + "sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/nginx/modsec/modsecurity.conf && " + "sed -i 's|SecAuditLog /var/log/modsec_audit.log|SecAuditLog /var/log/modsec_audit.log|' " + "/etc/nginx/modsec/modsecurity.conf && " + "cd /etc/nginx/modsec && " + "git clone --depth 1 https://github.com/coreruleset/coreruleset.git owasp-crs 2>&1 && " + "cp owasp-crs/crs-setup.conf.example owasp-crs/crs-setup.conf && " + "cat > /etc/nginx/modsec/main.conf << 'MODSECEOF'\n" + "Include /etc/nginx/modsec/modsecurity.conf\n" + "Include /etc/nginx/modsec/owasp-crs/crs-setup.conf\n" + "Include /etc/nginx/modsec/owasp-crs/rules/*.conf\n" + "MODSECEOF\n" + "echo 'ModSecurity installed with OWASP CRS. Add modsecurity on; modsecurity_rules_file /etc/nginx/modsec/main.conf; to your nginx server blocks.'" + ), + "scan": "tail -30 /var/log/modsec_audit.log 2>/dev/null || echo 'No ModSecurity logs yet'", + "uninstall": ( + "rm -rf /etc/nginx/modsec 2>&1 && " + "apt-get remove -y libmodsecurity3 nginx-module-modsecurity 2>/dev/null; " + "apt-get autoremove -y 2>&1 && " + "echo 'ModSecurity removed - remember to remove modsecurity directives from nginx configs'" + ), + }, + { + "name": "AIDE", + "desc": "Advanced Intrusion Detection Environment - file integrity monitoring", + "cat": "integrity", + "check": "aide --version 2>/dev/null", + "install": ( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y aide 2>&1 && " + "aideinit 2>&1 && " + "cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db 2>/dev/null || " + "cp /var/lib/aide/aide.db.new.gz /var/lib/aide/aide.db.gz 2>/dev/null && " + "echo 'AIDE installed and database initialized. Run scan to check for changes.'" + ), + "scan": "aide --check 2>&1 | tail -40", + "uninstall": ( + "apt-get remove -y aide 2>&1 && " + "apt-get autoremove -y 2>&1 && " + "rm -rf /var/lib/aide 2>/dev/null; " + "echo 'AIDE removed'" + ), + }, + { + "name": "Cowrie", + "desc": "SSH/Telnet honeypot - logs brute force attacks and attacker shell interactions", + "cat": "honeypot", + "check": "systemctl is-active cowrie 2>/dev/null", + "install": ( + "DEBIAN_FRONTEND=noninteractive apt-get update -qq && " + "DEBIAN_FRONTEND=noninteractive apt-get install -y git python3-venv python3-dev " + "libssl-dev libffi-dev build-essential 2>&1 && " + "id cowrie >/dev/null 2>&1 || useradd -r -m -d /opt/cowrie -s /bin/bash cowrie && " + "cd /opt/cowrie && " + "if [ ! -d /opt/cowrie/cowrie-git ]; then " + " sudo -u cowrie git clone https://github.com/cowrie/cowrie.git cowrie-git 2>&1; " + "fi && " + "cd /opt/cowrie/cowrie-git && " + "sudo -u cowrie python3 -m venv cowrie-env 2>&1 && " + "sudo -u cowrie ./cowrie-env/bin/pip install --upgrade pip 2>&1 && " + "sudo -u cowrie ./cowrie-env/bin/pip install -r requirements.txt 2>&1 && " + "sudo -u cowrie cp etc/cowrie.cfg.dist etc/cowrie.cfg && " + "sudo -u cowrie sed -i 's/^#\\?listen_endpoints = tcp:2222/listen_endpoints = tcp:2222/' etc/cowrie.cfg && " + "cat > /etc/systemd/system/cowrie.service << 'COWRIEEOF'\n" + "[Unit]\n" + "Description=Cowrie SSH/Telnet Honeypot\n" + "After=network.target\n" + "\n" + "[Service]\n" + "Type=simple\n" + "User=cowrie\n" + "Group=cowrie\n" + "WorkingDirectory=/opt/cowrie/cowrie-git\n" + "ExecStart=/opt/cowrie/cowrie-git/cowrie-env/bin/python /opt/cowrie/cowrie-git/bin/cowrie start -n\n" + "Restart=on-failure\n" + "\n" + "[Install]\n" + "WantedBy=multi-user.target\n" + "COWRIEEOF\n" + "systemctl daemon-reload 2>&1 && " + "systemctl enable cowrie 2>&1 && " + "systemctl start cowrie 2>&1 && " + "echo 'Cowrie honeypot installed and listening on port 2222. " + "Consider redirecting port 22 traffic with: iptables -t nat -A PREROUTING -p tcp --dport 22 -j REDIRECT --to-port 2222'" + ), + "scan": ( + "echo '=== COWRIE STATUS ===' && " + "systemctl status cowrie --no-pager 2>&1 | head -10 && " + "echo '' && echo '=== RECENT HONEYPOT ACTIVITY ===' && " + "tail -50 /opt/cowrie/cowrie-git/var/log/cowrie/cowrie.log 2>/dev/null || " + "tail -50 /opt/cowrie/var/log/cowrie/cowrie.log 2>/dev/null || " + "echo 'No honeypot logs yet'" + ), + "uninstall": ( + "systemctl stop cowrie 2>/dev/null; " + "systemctl disable cowrie 2>/dev/null; " + "rm -f /etc/systemd/system/cowrie.service && " + "systemctl daemon-reload 2>&1 && " + "rm -rf /opt/cowrie 2>&1 && " + "userdel -r cowrie 2>/dev/null; " + "echo 'Cowrie removed'" + ), + }, +] + +CATEGORIES = { + "antivirus": "Antivirus", + "rootkit": "Rootkit Detection", + "audit": "Security Auditing", + "ids": "Intrusion Detection", + "waf": "Web Application Firewall", + "integrity": "File Integrity", + "honeypot": "Honeypot", +} diff --git a/setec-web/ssh_client.py b/setec-web/ssh_client.py new file mode 100644 index 0000000..d63a263 --- /dev/null +++ b/setec-web/ssh_client.py @@ -0,0 +1,126 @@ +"""SSH client with optional E2E encryption. + +When E2E is enabled, commands are encrypted with the tunnel key before +being sent over SSH, and responses are decrypted on return. The Go agent +on the VPS handles decryption/execution/re-encryption. + +When E2E is disabled, commands run as plain text over SSH (still encrypted +by SSH transport, just no additional application-layer encryption). +""" + +import paramiko +import config +import json + +_client = None + + +def get_client(): + global _client + if _client and _client.get_transport() and _client.get_transport().is_active(): + return _client + cfg = config.load() + _client = paramiko.SSHClient() + _client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + key_path = cfg.get("ssh_key_path", "") + if not key_path: + raise RuntimeError("SSH key path not configured") + + # Try Ed25519 first, fall back to RSA + try: + key = paramiko.Ed25519Key.from_private_key_file(key_path) + except Exception: + try: + key = paramiko.RSAKey.from_private_key_file(key_path) + except Exception: + key = paramiko.ECDSAKey.from_private_key_file(key_path) + + _client.connect( + hostname=cfg["vps_host"], + port=cfg["vps_port"], + username=cfg["vps_user"], + pkey=key, + timeout=15, + ) + return _client + + +def run(cmd, timeout=30): + """Execute a command on the VPS. Uses E2E encryption when enabled.""" + import e2e + + if e2e.is_e2e_enabled(): + return _run_e2e(cmd, timeout) + else: + return _run_plain(cmd, timeout) + + +def _run_plain(cmd, timeout=30): + """Execute command via plain SSH (no E2E).""" + client = get_client() + _, stdout, stderr = client.exec_command(cmd, timeout=timeout) + out = stdout.read().decode("utf-8", errors="replace") + err = stderr.read().decode("utf-8", errors="replace") + code = stdout.channel.recv_exit_status() + return {"stdout": out, "stderr": err, "exit_code": code} + + +def _run_e2e(cmd, timeout=30): + """Execute command via E2E encrypted tunnel. + + 1. Encrypt command with tunnel key + 2. SSH: echo | setec-agent + 3. Agent decrypts, executes, encrypts response + 4. Decrypt response locally + """ + import e2e + + tunnel_key = e2e.get_tunnel_key() + + # Build the wrapped command + agent_cmd = e2e.wrap_command_for_agent(tunnel_key, cmd) + + # Execute via SSH + client = get_client() + _, stdout, stderr = client.exec_command(agent_cmd, timeout=timeout) + out = stdout.read().decode("utf-8", errors="replace").strip() + err = stderr.read().decode("utf-8", errors="replace").strip() + code = stdout.channel.recv_exit_status() + + # If agent returned an error on stderr, it's an agent-level failure + if code != 0 and not out: + return { + "stdout": "", + "stderr": f"E2E agent error: {err}" if err else "E2E agent returned non-zero with no output", + "exit_code": code, + } + + # Decrypt the response + try: + response = e2e.decrypt_response(tunnel_key, out) + return { + "stdout": response.get("stdout", ""), + "stderr": response.get("stderr", ""), + "exit_code": response.get("exit_code", 0), + } + except Exception as ex: + # If decryption fails, the output might be from a non-E2E command + # or the agent isn't installed. Return raw output with warning. + return { + "stdout": "", + "stderr": f"E2E decryption failed: {str(ex)}\nRaw output: {out[:200]}", + "exit_code": -1, + } + + +def run_plain_always(cmd, timeout=30): + """Always run without E2E — used for agent deployment and setup commands.""" + return _run_plain(cmd, timeout) + + +def close(): + global _client + if _client: + _client.close() + _client = None diff --git a/setec-web/ssl_audit.py b/setec-web/ssl_audit.py new file mode 100644 index 0000000..c540b41 --- /dev/null +++ b/setec-web/ssl_audit.py @@ -0,0 +1,105 @@ +# ssl_audit.py — SSL/TLS audit and certificate management commands + + +def ssl_check_cmd(domain): + """Return bash cmd to check SSL certificate details for a domain.""" + return ( + f"echo '=== Certificate Info ===' && " + f"echo | openssl s_client -servername {domain} -connect {domain}:443 2>/dev/null | " + f"openssl x509 -noout -subject -issuer -dates -serial -fingerprint 2>&1 && " + f"echo '' && echo '=== Certificate Chain ===' && " + f"echo | openssl s_client -servername {domain} -connect {domain}:443 -showcerts 2>/dev/null | " + f"grep -E '(subject|issuer|depth)' 2>&1" + ) + + +def ssl_expiry_cmd(domain): + """Return bash cmd to check certificate expiry date and days remaining.""" + return ( + f"EXPIRY=$(echo | openssl s_client -servername {domain} -connect {domain}:443 2>/dev/null | " + f"openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2) && " + f"EXPIRY_EPOCH=$(date -d \"$EXPIRY\" +%s 2>/dev/null) && " + f"NOW_EPOCH=$(date +%s) && " + f"DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 )) && " + f"echo \"Domain: {domain}\" && " + f"echo \"Expires: $EXPIRY\" && " + f"echo \"Days remaining: $DAYS_LEFT\" && " + f"if [ $DAYS_LEFT -lt 7 ]; then echo 'STATUS: CRITICAL'; " + f"elif [ $DAYS_LEFT -lt 30 ]; then echo 'STATUS: WARNING'; " + f"else echo 'STATUS: OK'; fi" + ) + + +def ssl_expiry_all_cmd(): + """Return bash cmd to check expiry for all certbot-managed certs.""" + return "certbot certificates 2>&1" + + +def ssl_grade_cmd(domain): + """Return bash cmd to audit TLS configuration quality.""" + return ( + f"echo '=== Protocol Support ===' && " + f"for proto in tls1 tls1_1 tls1_2 tls1_3; do " + f" result=$(echo | openssl s_client -$proto -connect {domain}:443 2>&1); " + f" if echo \"$result\" | grep -q 'Cipher is'; then " + f" echo \" $proto: ENABLED\"; " + f" else " + f" echo \" $proto: disabled\"; " + f" fi; " + f"done && " + f"echo '' && echo '=== Cipher Suites ===' && " + f"nmap --script ssl-enum-ciphers -p 443 {domain} 2>/dev/null | " + f"grep -E '(TLSv|cipher|strength|compressors)' || " + f"echo '(install nmap for cipher enumeration)' && " + f"echo '' && echo '=== Security Headers ===' && " + f"curl -sI https://{domain} 2>/dev/null | " + f"grep -iE '(strict-transport|content-security|x-frame|x-content-type|x-xss|referrer-policy|permissions-policy)' || " + f"echo ' No security headers found (BAD)' && " + f"echo '' && echo '=== HSTS ===' && " + f"curl -sI https://{domain} 2>/dev/null | grep -i strict-transport || echo ' HSTS: NOT SET (BAD)'" + ) + + +def ssl_renew_cmd(): + """Return bash cmd to force-renew all certbot certificates.""" + return "certbot renew --force-renewal 2>&1" + + +def ssl_renew_dry_cmd(): + """Return bash cmd to do a dry-run renewal check.""" + return "certbot renew --dry-run 2>&1" + + +def ssl_autorenew_status_cmd(): + """Return bash cmd to check if certbot auto-renewal is set up.""" + return ( + "echo '=== Certbot Timer ===' && " + "systemctl status certbot.timer 2>&1 || " + "echo 'No certbot timer found' && " + "echo '' && echo '=== Certbot Cron ===' && " + "grep -r certbot /etc/cron* 2>/dev/null || echo 'No certbot cron job found'" + ) + + +def security_headers_cmd(domain): + """Return bash cmd to add security headers to nginx config for a domain.""" + headers = ( + "add_header X-Frame-Options DENY always;\\n" + "add_header X-Content-Type-Options nosniff always;\\n" + "add_header X-XSS-Protection \\\"1; mode=block\\\" always;\\n" + "add_header Referrer-Policy strict-origin-when-cross-origin always;\\n" + "add_header Permissions-Policy \\\"camera=(), microphone=(), geolocation=()\\\" always;\\n" + "add_header Strict-Transport-Security \\\"max-age=31536000; includeSubDomains; preload\\\" always;" + ) + return ( + f"CONF=$(ls /etc/nginx/sites-available/{domain}* 2>/dev/null | head -1) && " + f"if [ -z \"$CONF\" ]; then echo 'No nginx config found for {domain}'; exit 1; fi && " + f"cp \"$CONF\" \"$CONF.bak.$(date +%Y%m%d_%H%M%S)\" && " + f"if ! grep -q 'X-Frame-Options' \"$CONF\"; then " + f" sed -i '/server_name/a\\ {headers}' \"$CONF\" && " + f" nginx -t 2>&1 && systemctl reload nginx && " + f" echo 'Security headers added to '$CONF; " + f"else " + f" echo 'Security headers already present in '$CONF; " + f"fi" + ) diff --git a/setec-web/static/setec_labs_logo.svg b/setec-web/static/setec_labs_logo.svg new file mode 100644 index 0000000..6e50045 --- /dev/null +++ b/setec-web/static/setec_labs_logo.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +SETEC LABS + + + + +SECURITY · RESEARCH · EXPLOITATION +v4.2.0 +SL-01 + \ No newline at end of file diff --git a/setec-web/templates/base.html b/setec-web/templates/base.html new file mode 100644 index 0000000..023b0db --- /dev/null +++ b/setec-web/templates/base.html @@ -0,0 +1,299 @@ + + + + + +SETEC LABS - {% block title %}Manager{% endblock %} + + + + +
+ {% block content %}{% endblock %} +
+ +{% block scripts %}{% endblock %} + + diff --git a/setec-web/templates/configs.html b/setec-web/templates/configs.html new file mode 100644 index 0000000..efd2672 --- /dev/null +++ b/setec-web/templates/configs.html @@ -0,0 +1,222 @@ +{% extends "base.html" %} +{% block title %}Config Editor{% endblock %} +{% block content %} +

[=] Config Editor

+ +
+
+
+
Quick Access
+

Nginx

+
+ + + + +
+

GitLab

+
+ +
+

Gitea

+
+ + +
+

SSH

+
+ + +
+

Mail

+
+ + + +
+

Security

+
+ + +
+

Docker

+
+ + +
+

System

+
+ + + + + +
+

Custom Path

+
+ + +
+
+ +
+
Directory Browser
+
Click a directory button above
+
+
+ +
+
+
+ Editor: none +
+ +
+ + + + + +
+
+ + +
+
+ +
+
Output
+
Ready. Click a config file to edit.
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/dashboard.html b/setec-web/templates/dashboard.html new file mode 100644 index 0000000..daa2551 --- /dev/null +++ b/setec-web/templates/dashboard.html @@ -0,0 +1,54 @@ +{% extends "base.html" %} +{% block title %}Dashboard{% endblock %} +{% block content %} +

[~] Dashboard

+ +
+ + + + +
+ +
+
+
Server Status
+
Click "Refresh Status" to load
+
+
+
Domain Status
+
Click "Check Domains" to load
+
+
+ +
+
Quick Actions
+
Ready.
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/detect.html b/setec-web/templates/detect.html new file mode 100644 index 0000000..f3192f1 --- /dev/null +++ b/setec-web/templates/detect.html @@ -0,0 +1,140 @@ +{% extends "base.html" %} +{% block title %}Service Detection{% endblock %} +{% block content %} +

[?] Service Detection

+ +
+ + +
+ +
+ + + +
+
Output
+
Click "Scan Server" to detect installed services
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/dns.html b/setec-web/templates/dns.html new file mode 100644 index 0000000..20d66f5 --- /dev/null +++ b/setec-web/templates/dns.html @@ -0,0 +1,101 @@ +{% extends "base.html" %} +{% block title %}DNS{% endblock %} +{% block content %} +

[@] DNS Management

+ +
+ +
+ +
+
+
DNS Records (Hostinger)
+
Loading...
+
+
+
+
Add Record
+ + + + + + +

+ +
+
+
Delete Record
+ + +

+ +
+
+
+ +
+
Output
+
Ready.
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/docker.html b/setec-web/templates/docker.html new file mode 100644 index 0000000..bfac86e --- /dev/null +++ b/setec-web/templates/docker.html @@ -0,0 +1,334 @@ +{% extends "base.html" %} +{% block title %}Docker{% endblock %} +{% block content %} +

[#] Docker Management

+ +
+ + + + + + + +
+ + +
+
+
Running Containers
+ + + +
NameImageStatusPorts / Web UIActions
Loading...
+
+ +
+
Container Logs
+
+ + + +
+
Select a container to view logs
+
+
+ + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/docs.html b/setec-web/templates/docs.html new file mode 100644 index 0000000..a7da354 --- /dev/null +++ b/setec-web/templates/docs.html @@ -0,0 +1,609 @@ +{% extends "base.html" %} +{% block title %}Documentation{% endblock %} +{% block content %} +

[^] Documentation

+ +
+ + + +
+ + + + +
+
User Manual
+
+ +

1. Introduction

+

SETEC LABS Manager is a web-based VPS management panel that connects to your Linux server over SSH. +It provides a terminal-style interface for managing security tools, firewalls, DNS, nginx, Docker, +email, and more — all from your browser.

+

Requirements: A Linux VPS (Debian/Ubuntu recommended), SSH key access, Python 3.10+

+ +

2. Installation

+
+# Clone the repository
+git clone https://repo.seteclabs.io/setec/setec-mgr.git
+cd setec-mgr

+# Install dependencies
+pip install -r requirements.txt

+# Start the manager
+python app.py

+# Open in browser
+http://localhost:5000 +
+ +

3. Initial Setup (Setup Wizard)

+

On first launch, click Setup Wizard in the sidebar. The wizard walks you through:

+
    +
  1. Terms of Service — Read and accept the disclaimer.
  2. +
  3. SSH Keys — Select existing keys or generate new ones with host-specific guidance.
  4. +
  5. VPS Connection — Enter your server IP, SSH username, port (2222 recommended), and key path.
  6. +
  7. DNS API — Select your hosting provider, enter your domain and API key.
  8. +
  9. Paths — Set web root and Docker Compose file location.
  10. +
  11. Connection Test — Verify SSH and API connectivity.
  12. +
+

You can re-run the wizard at any time. All settings are also editable from the Settings page.

+ +

4. Dashboard

+

The Dashboard shows a real-time overview of your server:

+
    +
  • System Info — Hostname, OS, kernel, uptime
  • +
  • Resource Usage — CPU, RAM, disk, swap
  • +
  • Network — Active connections, listening ports
  • +
  • Services — Status of key services (nginx, sshd, etc.)
  • +
+ +

5. Docker Management

+

Manage containers, images, volumes, and networks. Start/stop/restart containers, view logs, +pull images, and manage your Docker Compose stack.

+
+Note: SETEC runs services natively with systemd by default. Docker management is provided for +users who have containerized workloads. +
+ +

6. DNS Management

+

View, add, and delete DNS records through your hosting provider's API. Supports:

+
    +
  • A, AAAA, CNAME, MX, TXT, NS records
  • +
  • 10 hosting providers (see Host API / SSH Links tab)
  • +
  • Fallback to dig when API is unavailable
  • +
+ +

7. Nginx Management

+

Create and manage nginx virtual hosts, enable/disable sites, view access and error logs, +test configuration, and manage SSL certificates with Let's Encrypt (certbot).

+ +

8. SMTP / Email

+

Configure and manage mail services. View mail queue, check DKIM/SPF/DMARC records, +test email delivery, and manage Postfix configuration.

+ +

9. Firewall

+

The Firewall page (separate from Security) provides:

+
    +
  • Dashboard — Firewall activity overview and monitoring
  • +
  • UFW — Simplified firewall rule management
  • +
  • iptables — Advanced packet filtering rules
  • +
  • nftables — Modern netfilter framework management
  • +
  • firewalld — Zone-based firewall management
  • +
  • CSF — ConfigServer Security & Firewall
  • +
  • Migration — Convert between UFW and iptables with one click
  • +
+ +

10. Fail2Ban

+

Manage Fail2Ban jails, view banned IPs, check jail status, and configure ban rules +to protect against brute-force attacks on SSH, nginx, and other services.

+ +

11. Security Center

+

The Security page is your central hub for hardening and monitoring:

+ +

Hardening Tools

+
    +
  • SSH Hardening — Disable root login, enforce key auth, change port
  • +
  • Kernel Hardening — Sysctl tweaks for network and memory protection
  • +
  • Auto Updates — Enable unattended-upgrades for security patches
  • +
  • .sec Patch System — Apply SETEC-curated distro-specific security patches
  • +
+ +

Security Applications (each with full management tab)

+ + + + + + + + + + +
AppPurpose
ClamAVAntivirus scanning, quarantine management, scheduled scans
rkhunterRootkit detection, file property checks
chkrootkitAlternative rootkit scanner with expert mode
LynisSecurity auditing and hardening index scoring
OSSECHost-based intrusion detection (HIDS), log monitoring, alerts
ModSecurityWeb application firewall (WAF) for nginx, OWASP CRS rules
AIDEFile integrity monitoring, baseline comparison
CowrieSSH/Telnet honeypot for attacker monitoring
+

Each app tab provides: install/uninstall, status, configuration, scanning/auditing, logs, and scheduled tasks.

+ +

12. Detect

+

Server detection and fingerprinting. Identifies installed software, open ports, +running services, and potential security issues.

+ +

13. Configs

+

View and edit critical configuration files directly: sshd_config, nginx.conf, +jail.local, and other system configs with syntax-aware editing.

+ +

14. Files

+

Browse the server filesystem, view file contents, upload and download files, +manage permissions, and navigate directories.

+ +

15. Terminal

+

Direct SSH terminal access from the browser. Execute commands on your server +with full output display. Useful for tasks not covered by the GUI.

+ +

16. Settings

+

Configure all SETEC Manager settings:

+
    +
  • VPS Connection — Host, user, port, SSH key path
  • +
  • Hosting Provider API — Provider selection, API key, documentation links
  • +
  • Domain & Paths — Domain, web root, compose path
  • +
+ +

17. Front Page

+

Manage the public-facing landing page for your domain. Edit content, +configure styling, and deploy updates.

+ +

18. Keyboard Shortcuts & Tips

+
    +
  • All actions use AJAX — the page never fully reloads
  • +
  • Output panels are scrollable; long scan outputs won't overflow
  • +
  • Red text = error, yellow text = warning, green text = success
  • +
  • Every destructive action (uninstall, delete, purge) requires confirmation
  • +
  • SSH connection is shared — the manager reuses a single SSH session
  • +
+ +

19. Getting Help

+ + +
+
+ + + + + + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/fail2ban.html b/setec-web/templates/fail2ban.html new file mode 100644 index 0000000..d58216b --- /dev/null +++ b/setec-web/templates/fail2ban.html @@ -0,0 +1,168 @@ +{% extends "base.html" %} +{% block title %}Fail2Ban{% endblock %} +{% block content %} +

[!] Fail2Ban

+ +
+ + + + + + +
+ +
+
+
Status
+
Loading...
+
+
+
Jail Details
+
+ +
+
Select a jail
+
+
+ +
+
+
Ban IP
+ + + + +

+ +
+
+
Unban IP
+ + + + +

+ +
+
+ + + +
+
Output / Log
+
Ready.
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/files.html b/setec-web/templates/files.html new file mode 100644 index 0000000..b1a1926 --- /dev/null +++ b/setec-web/templates/files.html @@ -0,0 +1,93 @@ +{% extends "base.html" %} +{% block title %}Files{% endblock %} +{% block content %} +

[/] File Manager

+ +
+ + + + +
+ +
+
+
Directory Listing
+
+ Enter a path and click List +
+
+
+
+
File Editor
+ + + +
+ + +
+
+
+ +
+
Output
+
Ready.
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/firewall.html b/setec-web/templates/firewall.html new file mode 100644 index 0000000..7efc447 --- /dev/null +++ b/setec-web/templates/firewall.html @@ -0,0 +1,990 @@ +{% extends "base.html" %} +{% block title %}Firewall{% endblock %} +{% block content %} +

[|] Firewall Manager

+ + +
+ + + + + + + + +
+ + +
+
+
+
Firewall Detection
+
+ +
+
+
Open Ports
+
+ +
+
+
+
+
Active Connections
+
+ +
+
+
Connection Stats by State
+
+ +
+
+
+
+
Top Connections by IP
+
+ +
+
+
Recent Blocked / Dropped
+
+ +
+
+
+
Firewall Log
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/frontpage.html b/setec-web/templates/frontpage.html new file mode 100644 index 0000000..95cb599 --- /dev/null +++ b/setec-web/templates/frontpage.html @@ -0,0 +1,182 @@ +{% extends "base.html" %} +{% block title %}Front Page Editor{% endblock %} +{% block content %} +

[<] Front Page Editor

+ +
+ + + +
+ +
+ +
+
+
Site Files
+
+
+
+ + +
+
+ No file selected + +
+ + +
+
+ +
+
+
+ + +{% endblock %} diff --git a/setec-web/templates/nginx.html b/setec-web/templates/nginx.html new file mode 100644 index 0000000..a076a2b --- /dev/null +++ b/setec-web/templates/nginx.html @@ -0,0 +1,107 @@ +{% extends "base.html" %} +{% block title %}Nginx{% endblock %} +{% block content %} +

[>] Nginx / Subdomains

+ +
+ + +
+ +
+
+
Active Sites
+
Loading...
+
+
+
+
Add Subdomain
+ + + + +
+ + +
+ +
+ +
+
+
SSL Certificate
+ + +

+ +
+
+
View Config
+ + +

+ +
+
+
+ +
+
Output
+
Ready.
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/security.html b/setec-web/templates/security.html new file mode 100644 index 0000000..8e17338 --- /dev/null +++ b/setec-web/templates/security.html @@ -0,0 +1,1598 @@ +{% extends "base.html" %} +{% block title %}Security{% endblock %} +{% block content %} +

[!] Security Center

+ + +
+ + + + + + + + + + + + + + +
+ + +
+
+
+
SSH Hardening
+
+ + + + +
+ + +
+
+
+
Kernel Hardening
+
+ + +
+
+
+
+
Automatic Security Updates
+
+ + +
+
+
User & Permission Audit
+
+
+ + + + +
+
+
+
+
.sec Patch Updates
+
+
+ + +
+
+ + + + + + + +
+
+
+
Open Port Scan
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/settings.html b/setec-web/templates/settings.html new file mode 100644 index 0000000..36150c7 --- /dev/null +++ b/setec-web/templates/settings.html @@ -0,0 +1,216 @@ +{% extends "base.html" %} +{% block title %}Settings{% endblock %} +{% block content %} +

[%] Settings

+ +
+
+
VPS Connection
+ + + + + + + + +
+
+
Domain & Paths
+ + + + + + +
+
+ +
+
Hosting Provider API
+

Select your hosting provider and enter API credentials for DNS management.

+ + +
+ + +
+
+ +
+
E2E SSH Encryption
+

+ Encrypts all SSH commands with AES-256-GCM before transport. Requires the setec-agent on the VPS. +

+
Loading...
+
+ + + +
+ +
+ +
+ + + +
+ +
+
Output
+
Ready.
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/smtp.html b/setec-web/templates/smtp.html new file mode 100644 index 0000000..d5ca547 --- /dev/null +++ b/setec-web/templates/smtp.html @@ -0,0 +1,161 @@ +{% extends "base.html" %} +{% block title %}SMTP{% endblock %} +{% block content %} +

[*] SMTP / Mail

+ +
+ + + + + + + +
+ + +
+
+
+
SMTP Status
+
Loading...
+
+
+
DNS Records (SPF/DKIM/DMARC)
+
Click "Check DNS"
+
+
+ +
+
Quick Test
+ + +
+
+ + + + + + + +
+
Output
+
Ready.
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/terminal.html b/setec-web/templates/terminal.html new file mode 100644 index 0000000..6beae30 --- /dev/null +++ b/setec-web/templates/terminal.html @@ -0,0 +1,83 @@ +{% extends "base.html" %} +{% block title %}Terminal{% endblock %} +{% block content %} +

[$] SSH Terminal

+ +
+
Remote Shell (root@VPS)
+
+
+ $ + + +
+
+ +
+
Quick Commands
+
+ + + + + + + + + + +
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/templates/wizard.html b/setec-web/templates/wizard.html new file mode 100644 index 0000000..e410eaf --- /dev/null +++ b/setec-web/templates/wizard.html @@ -0,0 +1,516 @@ +{% extends "base.html" %} +{% block title %}Setup Wizard{% endblock %} +{% block content %} +

[+] Setup Wizard

+ +
+ [1] Terms → + [2] SSH Keys → + [3] VPS → + [4] API → + [5] Paths → + [6] Test +
+ + + + +
+
License Agreement & Terms of Use
+
+

IMPORTANT - READ BEFORE CONTINUING

+ +

1. RESTRICTED USE LICENSE
+ This software is licensed for use by private individuals, independent security + researchers, and non-governmental organizations ONLY. By using this software you + affirm that you are not acting on behalf of, employed by, contracted by, or otherwise + affiliated with any:
+ • Law enforcement agency (local, state, federal, or international)
+ • Government agency or department (civilian or military)
+ • Intelligence service (domestic or foreign)
+ • Government contractor performing work for any of the above
+
+ Use of this software by any of the above entities or their agents is strictly + prohibited and constitutes a violation of this license. This restriction applies + regardless of the purpose, including but not limited to: investigations, surveillance, + offensive operations, defensive operations, or "research." No exceptions.

+ +

2. NO WARRANTY
+ This software is provided "AS IS" without warranty of any kind, express or implied. + SETEC LABS makes no guarantees regarding reliability, availability, or fitness for + any particular purpose. Use at your own risk.

+ +

3. NO GUARANTEE OF SUPPORT
+ While the community may assist, there is no obligation to provide support, updates, + or bug fixes. This is free, open-source software maintained by volunteers.

+ +

4. LIMITATION OF LIABILITY
+ Under no circumstances shall SETEC LABS, its contributors, or the darkHal group be + liable for any direct, indirect, incidental, or consequential damages arising from + the use of this software. This includes but is not limited to: data loss, system + downtime, security breaches, or misconfiguration of your server.

+ +

5. IF YOU PAID FOR THIS SOFTWARE, YOU WERE SCAMMED
+ SETEC LABS Manager is 100% free and open source. If someone charged you + money for this application, you were ripped off and likely received a version bundled + with malware. Delete it immediately.

+ +

6. OFFICIAL DOWNLOAD SOURCES
+ Only download SETEC applications from these trusted sources:
+ • repo.seteclabs.io (Official Gitea repository)
+ • github.com/DigiJEth (Official GitHub mirror)
+ Any other source is unauthorized and potentially dangerous.

+ +

7. ROOT ACCESS WARNING
+ This software executes commands on remote servers via SSH with the privileges of the + configured user. Misconfiguration can result in data loss or security vulnerabilities. + You are solely responsible for the actions performed through this tool.

+ +

+ SETEC LABS Manager • Free Software • darkHal Group • For the people, not the state.

+
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/setec-web/wsgi.py b/setec-web/wsgi.py new file mode 100644 index 0000000..7a7c3c2 --- /dev/null +++ b/setec-web/wsgi.py @@ -0,0 +1,53 @@ +"""Production WSGI entry point — Waitress (Windows) or Gunicorn (Linux).""" + +import sys +import os +import config + +# Add the setec-web directory to path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from app import app + + +def main(): + cfg = config.load() + host = "127.0.0.1" + port = cfg.get("flask_port", 5000) + + print(f"SETEC LABS Manager starting on http://{host}:{port}") + print(f"Production WSGI server") + + if sys.platform == "win32": + try: + import waitress + print("Using Waitress (Windows)") + waitress.serve(app, host=host, port=port, threads=4) + except ImportError: + print("WARNING: waitress not installed, falling back to Flask dev server") + print("Install with: pip install waitress") + app.run(host=host, port=port, debug=False) + else: + try: + import gunicorn # noqa: F401 + print("Using Gunicorn (Linux)") + # For gunicorn, use CLI: gunicorn -w 4 -b 127.0.0.1:5000 wsgi:app + # Fallback to waitress if gunicorn imported but we're not in CLI mode + try: + import waitress + waitress.serve(app, host=host, port=port, threads=4) + except ImportError: + app.run(host=host, port=port, debug=False) + except ImportError: + try: + import waitress + print("Using Waitress (Linux)") + waitress.serve(app, host=host, port=port, threads=4) + except ImportError: + print("WARNING: No production server found, falling back to Flask dev server") + print("Install with: pip install waitress OR pip install gunicorn") + app.run(host=host, port=port, debug=False) + + +if __name__ == "__main__": + main() diff --git a/updates/sec/debian12_0326.sec b/updates/sec/debian12_0326.sec new file mode 100644 index 0000000..4860ca1 --- /dev/null +++ b/updates/sec/debian12_0326.sec @@ -0,0 +1,55 @@ +# Setec Labs Security Update — Debian 12 Bookworm +# Release: 2026-03 +# Target: Debian 12 (Bookworm) +# Author: seteclabs.io +# Applied via: sec_updates.py parse_and_apply_cmd() + +[packages] +# Critical security patches for March 2026 +apt-get update -qq +apt-get install -y --only-upgrade openssl libssl3 +apt-get install -y --only-upgrade libgnutls30 +apt-get install -y --only-upgrade openssh-server openssh-client +apt-get install -y --only-upgrade curl libcurl4 +apt-get install -y --only-upgrade sudo +apt-get install -y --only-upgrade systemd libsystemd0 +apt-get install -y --only-upgrade linux-image-amd64 +apt-get install -y --only-upgrade bind9-dnsutils bind9-host +apt-get install -y --only-upgrade nginx +apt-get install -y --only-upgrade python3 libpython3-stdlib +apt-get install -y --only-upgrade git +apt-get install -y --only-upgrade vim-common xxd + +[sysctl] +# Harden network stack +net.ipv4.tcp_syncookies = 1 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 +net.ipv4.conf.all.accept_source_route = 0 +net.ipv4.icmp_echo_ignore_broadcasts = 1 +kernel.randomize_va_space = 2 +fs.protected_hardlinks = 1 +fs.protected_symlinks = 1 +kernel.kptr_restrict = 1 +kernel.dmesg_restrict = 1 + +[services] +systemctl restart ssh +systemctl restart nginx +systemctl daemon-reexec + +[files] +chmod 600 /etc/ssh/sshd_config +chmod 700 /root/.ssh +chmod 644 /etc/sysctl.d/99-setec-update.conf +chown root:root /etc/sysctl.d/99-setec-update.conf + +[custom] +# Cleanup old kernels +bash:-:apt-get autoremove -y --purge 2>&1 +# Verify no known vulnerable packages remain +bash:-:apt-get -s upgrade 2>&1 | grep -c 'upgraded' || echo '0 remaining upgrades' diff --git a/updates/sec/debian13_0326.sec b/updates/sec/debian13_0326.sec new file mode 100644 index 0000000..2899fdf --- /dev/null +++ b/updates/sec/debian13_0326.sec @@ -0,0 +1,53 @@ +# Setec Labs Security Update — Debian 13 Trixie +# Release: 2026-03 +# Target: Debian 13 (Trixie) +# Author: seteclabs.io +# Note: Trixie is the current testing/stable release + +[packages] +apt-get update -qq +apt-get install -y --only-upgrade openssl libssl3t64 +apt-get install -y --only-upgrade libgnutls30t64 +apt-get install -y --only-upgrade openssh-server openssh-client +apt-get install -y --only-upgrade curl libcurl4t64 +apt-get install -y --only-upgrade sudo +apt-get install -y --only-upgrade systemd libsystemd0 +apt-get install -y --only-upgrade linux-image-amd64 +apt-get install -y --only-upgrade nginx +apt-get install -y --only-upgrade python3 python3-minimal +apt-get install -y --only-upgrade git +apt-get install -y --only-upgrade vim-common xxd +apt-get install -y --only-upgrade libc6 + +[sysctl] +net.ipv4.tcp_syncookies = 1 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 +net.ipv4.conf.all.accept_source_route = 0 +net.ipv4.icmp_echo_ignore_broadcasts = 1 +kernel.randomize_va_space = 2 +fs.protected_hardlinks = 1 +fs.protected_symlinks = 1 +kernel.kptr_restrict = 1 +kernel.dmesg_restrict = 1 +# Trixie supports unprivileged userns restrictions +kernel.unprivileged_userns_clone = 0 + +[services] +systemctl restart ssh +systemctl restart nginx +systemctl daemon-reexec + +[files] +chmod 600 /etc/ssh/sshd_config +chmod 700 /root/.ssh +chmod 644 /etc/sysctl.d/99-setec-update.conf +chown root:root /etc/sysctl.d/99-setec-update.conf + +[custom] +bash:-:apt-get autoremove -y --purge 2>&1 +bash:-:apt-get -s upgrade 2>&1 | grep -c 'upgraded' || echo '0 remaining upgrades' diff --git a/updates/sec/ubuntu2204_0326.sec b/updates/sec/ubuntu2204_0326.sec new file mode 100644 index 0000000..87ba961 --- /dev/null +++ b/updates/sec/ubuntu2204_0326.sec @@ -0,0 +1,55 @@ +# Setec Labs Security Update — Ubuntu 22.04 LTS Jammy +# Release: 2026-03 +# Target: Ubuntu 22.04 (Jammy Jellyfish) +# Author: seteclabs.io +# EOL: April 2027 (standard), April 2032 (ESM) + +[packages] +apt-get update -qq +apt-get install -y --only-upgrade openssl libssl3 +apt-get install -y --only-upgrade libgnutls30 +apt-get install -y --only-upgrade openssh-server openssh-client +apt-get install -y --only-upgrade curl libcurl4 +apt-get install -y --only-upgrade sudo +apt-get install -y --only-upgrade systemd libsystemd0 +apt-get install -y --only-upgrade linux-image-generic +apt-get install -y --only-upgrade nginx +apt-get install -y --only-upgrade python3 python3-minimal +apt-get install -y --only-upgrade git +apt-get install -y --only-upgrade vim-common xxd +apt-get install -y --only-upgrade ca-certificates +apt-get install -y --only-upgrade apparmor + +[sysctl] +net.ipv4.tcp_syncookies = 1 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 +net.ipv4.conf.all.accept_source_route = 0 +net.ipv4.icmp_echo_ignore_broadcasts = 1 +kernel.randomize_va_space = 2 +fs.protected_hardlinks = 1 +fs.protected_symlinks = 1 +kernel.kptr_restrict = 1 +kernel.dmesg_restrict = 1 + +[services] +systemctl restart ssh +systemctl restart nginx +systemctl restart apparmor +systemctl daemon-reexec + +[files] +chmod 600 /etc/ssh/sshd_config +chmod 700 /root/.ssh +chmod 644 /etc/sysctl.d/99-setec-update.conf +chown root:root /etc/sysctl.d/99-setec-update.conf + +[custom] +bash:-:apt-get autoremove -y --purge 2>&1 +bash:-:apt-get -s upgrade 2>&1 | grep -c 'upgraded' || echo '0 remaining upgrades' +# Check Ubuntu Pro/ESM status +bash:-:pro status 2>/dev/null || echo 'Ubuntu Pro not configured' diff --git a/updates/sec/ubuntu2210_0326.sec b/updates/sec/ubuntu2210_0326.sec new file mode 100644 index 0000000..bade2eb --- /dev/null +++ b/updates/sec/ubuntu2210_0326.sec @@ -0,0 +1,49 @@ +# Setec Labs Security Update — Ubuntu 22.10 Kinetic +# Release: 2026-03 +# Target: Ubuntu 22.10 (Kinetic Kudu) +# Author: seteclabs.io +# +# !! WARNING: Ubuntu 22.10 reached End of Life on July 20, 2023 !! +# No further security patches are available from Canonical. +# This .sec file provides upgrade guidance only. + +[packages] +# IMPORTANT: 22.10 repos are archived. Update sources first if needed. +# sed -i 's|archive.ubuntu.com|old-releases.ubuntu.com|g' /etc/apt/sources.list +# sed -i 's|security.ubuntu.com|old-releases.ubuntu.com|g' /etc/apt/sources.list +apt-get update -qq +apt-get install -y --only-upgrade openssl libssl3 +apt-get install -y --only-upgrade openssh-server openssh-client +apt-get install -y --only-upgrade curl libcurl4 +apt-get install -y --only-upgrade sudo + +[sysctl] +# Minimal hardening — upgrade ASAP +net.ipv4.tcp_syncookies = 1 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +kernel.randomize_va_space = 2 +fs.protected_hardlinks = 1 +fs.protected_symlinks = 1 + +[services] +systemctl restart ssh + +[files] +chmod 600 /etc/ssh/sshd_config + +[custom] +# Display EOL warning prominently +bash:-:echo '==========================================' +bash:-:echo 'WARNING: Ubuntu 22.10 is END OF LIFE' +bash:-:echo 'No security patches since July 2023!' +bash:-:echo '==========================================' +bash:-:echo '' +bash:-:echo 'Recommended upgrade path:' +bash:-:echo ' 22.10 -> 23.04 -> 23.10 -> 24.04 LTS' +bash:-:echo '' +bash:-:echo 'Or fresh install Ubuntu 24.04 LTS (recommended)' +bash:-:echo '' +bash:-:echo 'To begin upgrade: do-release-upgrade' +bash:-:echo '==========================================' diff --git a/updates/sec/ubuntu2404_0326.sec b/updates/sec/ubuntu2404_0326.sec new file mode 100644 index 0000000..755c992 --- /dev/null +++ b/updates/sec/ubuntu2404_0326.sec @@ -0,0 +1,57 @@ +# Setec Labs Security Update — Ubuntu 24.04 LTS Noble +# Release: 2026-03 +# Target: Ubuntu 24.04 (Noble Numbat) +# Author: seteclabs.io +# EOL: April 2029 (standard), April 2034 (ESM) + +[packages] +apt-get update -qq +apt-get install -y --only-upgrade openssl libssl3t64 +apt-get install -y --only-upgrade libgnutls30t64 +apt-get install -y --only-upgrade openssh-server openssh-client +apt-get install -y --only-upgrade curl libcurl4t64 +apt-get install -y --only-upgrade sudo +apt-get install -y --only-upgrade systemd libsystemd0 +apt-get install -y --only-upgrade linux-image-generic +apt-get install -y --only-upgrade nginx +apt-get install -y --only-upgrade python3 python3-minimal +apt-get install -y --only-upgrade git +apt-get install -y --only-upgrade vim-common xxd +apt-get install -y --only-upgrade ca-certificates +apt-get install -y --only-upgrade apparmor +apt-get install -y --only-upgrade libc6 + +[sysctl] +net.ipv4.tcp_syncookies = 1 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 +net.ipv4.conf.all.accept_source_route = 0 +net.ipv4.icmp_echo_ignore_broadcasts = 1 +kernel.randomize_va_space = 2 +fs.protected_hardlinks = 1 +fs.protected_symlinks = 1 +kernel.kptr_restrict = 1 +kernel.dmesg_restrict = 1 +# Noble supports unprivileged userns restrictions +kernel.apparmor_restrict_unprivileged_userns = 1 + +[services] +systemctl restart ssh +systemctl restart nginx +systemctl restart apparmor +systemctl daemon-reexec + +[files] +chmod 600 /etc/ssh/sshd_config +chmod 700 /root/.ssh +chmod 644 /etc/sysctl.d/99-setec-update.conf +chown root:root /etc/sysctl.d/99-setec-update.conf + +[custom] +bash:-:apt-get autoremove -y --purge 2>&1 +bash:-:apt-get -s upgrade 2>&1 | grep -c 'upgraded' || echo '0 remaining upgrades' +bash:-:pro status 2>/dev/null || echo 'Ubuntu Pro not configured' diff --git a/updates/sec/ubuntu2410_0326.sec b/updates/sec/ubuntu2410_0326.sec new file mode 100644 index 0000000..395b051 --- /dev/null +++ b/updates/sec/ubuntu2410_0326.sec @@ -0,0 +1,57 @@ +# Setec Labs Security Update — Ubuntu 24.10 Oracular +# Release: 2026-03 +# Target: Ubuntu 24.10 (Oracular Oriole) +# Author: seteclabs.io +# EOL: July 2025 — approaching end of life, plan upgrade to 25.04 or 24.04 LTS + +[packages] +apt-get update -qq +apt-get install -y --only-upgrade openssl libssl3t64 +apt-get install -y --only-upgrade libgnutls30t64 +apt-get install -y --only-upgrade openssh-server openssh-client +apt-get install -y --only-upgrade curl libcurl4t64 +apt-get install -y --only-upgrade sudo +apt-get install -y --only-upgrade systemd libsystemd0 +apt-get install -y --only-upgrade linux-image-generic +apt-get install -y --only-upgrade nginx +apt-get install -y --only-upgrade python3 python3-minimal +apt-get install -y --only-upgrade git +apt-get install -y --only-upgrade vim-common xxd +apt-get install -y --only-upgrade ca-certificates +apt-get install -y --only-upgrade apparmor +apt-get install -y --only-upgrade libc6 + +[sysctl] +net.ipv4.tcp_syncookies = 1 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv4.conf.all.send_redirects = 0 +net.ipv6.conf.all.accept_redirects = 0 +net.ipv4.conf.all.accept_source_route = 0 +net.ipv4.icmp_echo_ignore_broadcasts = 1 +kernel.randomize_va_space = 2 +fs.protected_hardlinks = 1 +fs.protected_symlinks = 1 +kernel.kptr_restrict = 1 +kernel.dmesg_restrict = 1 +kernel.apparmor_restrict_unprivileged_userns = 1 + +[services] +systemctl restart ssh +systemctl restart nginx +systemctl restart apparmor +systemctl daemon-reexec + +[files] +chmod 600 /etc/ssh/sshd_config +chmod 700 /root/.ssh +chmod 644 /etc/sysctl.d/99-setec-update.conf +chown root:root /etc/sysctl.d/99-setec-update.conf + +[custom] +bash:-:apt-get autoremove -y --purge 2>&1 +bash:-:apt-get -s upgrade 2>&1 | grep -c 'upgraded' || echo '0 remaining upgrades' +# Reminder: 24.10 is a short-term release +bash:-:echo 'NOTE: Ubuntu 24.10 support ends July 2025. Consider upgrading to 25.04 or 24.04 LTS.'