Initial commit — SETEC LABS Manager (Setec_CDM)

Flask-based VPS management panel with SSH remote command execution.
Includes E2E encrypted SSH tunnel (AES-256-GCM + Go agent), setup wizard,
security hardening tools, DNS management, firewall configs, monitoring,
backup, and .sec patch update system.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DigiJ
2026-03-13 12:39:02 -07:00
commit 9e839ee826
62 changed files with 14605 additions and 0 deletions

3
setec-web/agent/go.mod Normal file
View File

@@ -0,0 +1,3 @@
module setec-agent
go 1.21

189
setec-web/agent/main.go Normal file
View File

@@ -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)
}

141
setec-web/aide.py Normal file
View File

@@ -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'"
)

2724
setec-web/app.py Normal file

File diff suppressed because it is too large Load Diff

69
setec-web/audit.py Normal file
View File

@@ -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

179
setec-web/backup.py Normal file
View File

@@ -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'"
)

106
setec-web/chkrootkit.py Normal file
View File

@@ -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.'"
)

187
setec-web/clamav.py Normal file
View File

@@ -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'"
)

61
setec-web/config.py Normal file
View File

@@ -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

175
setec-web/cowrie.py Normal file
View File

@@ -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'"
)

134
setec-web/csf.py Normal file
View File

@@ -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"

464
setec-web/ddos.py Normal file
View File

@@ -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}'"
)

437
setec-web/detector.py Normal file
View File

@@ -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

110
setec-web/dns_client.py Normal file
View File

@@ -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"]}

709
setec-web/docker_store.py Normal file
View File

@@ -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))

76
setec-web/e2e.py Normal file
View File

@@ -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

96
setec-web/firewalld.py Normal file
View File

@@ -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"
)

220
setec-web/hardening.py Normal file
View File

@@ -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'"
)

105
setec-web/hosting.py Normal file
View File

@@ -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()
]

209
setec-web/iptables.py Normal file
View File

@@ -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'"

98
setec-web/lynis.py Normal file
View File

@@ -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"

188
setec-web/modsecurity.py Normal file
View File

@@ -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=<script>alert(1)</script>' && "
"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"
)

359
setec-web/monitoring.py Normal file
View File

@@ -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'"
)

110
setec-web/nftables.py Normal file
View File

@@ -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"
)

113
setec-web/ossec.py Normal file
View File

@@ -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 '<active-response>' /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.'"
)

View File

@@ -0,0 +1,4 @@
flask>=3.0,<4.0
paramiko>=3.4,<4.0
waitress>=3.0,<4.0
requests>=2.31,<3.0

152
setec-web/rkhunter.py Normal file
View File

@@ -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'"
)

13
setec-web/run.bat Normal file
View File

@@ -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

101
setec-web/sanitize.py Normal file
View File

@@ -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

141
setec-web/sec_updates.py Normal file
View File

@@ -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"
)

242
setec-web/security_apps.py Normal file
View File

@@ -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",
}

126
setec-web/ssh_client.py Normal file
View File

@@ -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 <encrypted> | 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

105
setec-web/ssl_audit.py Normal file
View File

@@ -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"
)

View File

@@ -0,0 +1,67 @@
<svg width="100%" viewBox="0 0 680 392" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="hx"><polygon points="462,182 401,288 279,288 218,182 279,76 401,76"/></clipPath>
</defs>
<path d="M462,182 L508,182 L508,152 L536,152" fill="none" stroke="#D4921A" stroke-width="1.5" opacity="0.52"/>
<rect x="533" y="146" width="7" height="7" fill="#D4921A" opacity="0.65"/>
<rect x="505" y="146" width="5" height="5" fill="#D4921A" opacity="0.38"/>
<circle cx="545" cy="162" r="2" fill="#D4921A" opacity="0.4"/>
<path d="M218,182 L172,182 L172,152 L144,152" fill="none" stroke="#D4921A" stroke-width="1.5" opacity="0.52"/>
<rect x="137" y="146" width="7" height="7" fill="#D4921A" opacity="0.65"/>
<rect x="169" y="146" width="5" height="5" fill="#D4921A" opacity="0.38"/>
<circle cx="135" cy="162" r="2" fill="#D4921A" opacity="0.4"/>
<path d="M401,76 L430,47 L458,47" fill="none" stroke="#1DC9A4" stroke-width="1.5" opacity="0.45"/>
<rect x="455" y="41" width="7" height="7" fill="#1DC9A4" opacity="0.6"/>
<circle cx="441" cy="40" r="2" fill="#1DC9A4" opacity="0.35"/>
<path d="M279,76 L250,47 L222,47" fill="none" stroke="#1DC9A4" stroke-width="1.5" opacity="0.45"/>
<rect x="215" y="41" width="7" height="7" fill="#1DC9A4" opacity="0.6"/>
<circle cx="239" cy="40" r="2" fill="#1DC9A4" opacity="0.35"/>
<path d="M401,288 L426,313" fill="none" stroke="#D4921A" stroke-width="1" opacity="0.32"/>
<path d="M279,288 L254,313" fill="none" stroke="#D4921A" stroke-width="1" opacity="0.32"/>
<polygon points="462,182 401,288 279,288 218,182 279,76 401,76" fill="#060610" stroke="#D4921A" stroke-width="2.2"/>
<g clip-path="url(#hx)" opacity="0.05">
<line x1="218" y1="94" x2="462" y2="94" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="110" x2="462" y2="110" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="126" x2="462" y2="126" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="142" x2="462" y2="142" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="158" x2="462" y2="158" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="174" x2="462" y2="174" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="190" x2="462" y2="190" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="206" x2="462" y2="206" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="222" x2="462" y2="222" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="238" x2="462" y2="238" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="254" x2="462" y2="254" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="270" x2="462" y2="270" stroke="#D4921A" stroke-width="1"/>
<line x1="218" y1="286" x2="462" y2="286" stroke="#D4921A" stroke-width="1"/>
</g>
<polygon points="340,92 418,137 418,227 340,272 262,227 262,137" fill="none" stroke="#1DC9A4" stroke-width="0.75" opacity="0.26"/>
<path d="M340,103 L368,108 L390,121 L408,138 L420,160 L422,183 L412,206 L394,221 L367,229 L359,234 L340,239 L321,234 L313,229 L286,221 L268,206 L258,183 L260,160 L272,138 L290,121 L312,108 Z" fill="#0C0C1C" stroke="#D4921A" stroke-width="1.8"/>
<path d="M340,113 L360,118 L378,130 L391,146 L395,166 L386,182" fill="none" stroke="#1DC9A4" stroke-width="0.8" opacity="0.32"/>
<path d="M340,113 L320,118 L302,130 L289,146 L285,166 L294,182" fill="none" stroke="#1DC9A4" stroke-width="0.8" opacity="0.32"/>
<polygon points="340,121 351,133 340,145 329,133" fill="#161626" stroke="#D4921A" stroke-width="0.9" opacity="0.68"/>
<polygon points="365,133 375,145 365,157 355,145" fill="#161626" stroke="#1DC9A4" stroke-width="0.65" opacity="0.42"/>
<polygon points="315,133 325,145 315,157 305,145" fill="#161626" stroke="#1DC9A4" stroke-width="0.65" opacity="0.42"/>
<polygon points="387,150 397,162 387,174 377,162" fill="#161626" stroke="#1DC9A4" stroke-width="0.55" opacity="0.3"/>
<polygon points="293,150 303,162 293,174 283,162" fill="#161626" stroke="#1DC9A4" stroke-width="0.55" opacity="0.3"/>
<polygon points="340,160 357,167 357,194 340,202 323,194 323,167" fill="#0E0E1E" stroke="#1E1E32" stroke-width="0.9"/>
<line x1="340" y1="160" x2="340" y2="200" stroke="#D4921A" stroke-width="0.55" opacity="0.38"/>
<polygon points="295,149 312,163 295,177 278,163" fill="#E89920"/>
<polygon points="295,153 298,163 295,173 292,163" fill="#050510"/>
<polygon points="295,145 316,163 295,181 274,163" fill="none" stroke="#E89920" stroke-width="0.9" opacity="0.3"/>
<polygon points="385,149 402,163 385,177 368,163" fill="#E89920"/>
<polygon points="385,153 388,163 385,173 382,163" fill="#050510"/>
<polygon points="385,145 406,163 385,181 364,163" fill="none" stroke="#E89920" stroke-width="0.9" opacity="0.3"/>
<circle cx="327" cy="204" r="2.2" fill="#D4921A" opacity="0.55"/>
<circle cx="353" cy="204" r="2.2" fill="#D4921A" opacity="0.55"/>
<path d="M340,239 L340,259 L328,276 M340,259 L352,276" fill="none" stroke="#B01E28" stroke-width="2.5" stroke-linecap="round"/>
<rect x="48" y="309" width="584" height="74" rx="2" fill="#050510" opacity="0.93"/>
<line x1="48" y1="309" x2="632" y2="309" stroke="#D4921A" stroke-width="0.8" opacity="0.55"/>
<text x="340" y="341" text-anchor="middle" font-family="var(--font-mono)" font-size="29" font-weight="700" fill="#D4921A" letter-spacing="7">SETEC LABS</text>
<line x1="68" y1="352" x2="218" y2="352" stroke="#D4921A" stroke-width="0.6" opacity="0.48"/>
<circle cx="220" cy="352" r="2.2" fill="#1DC9A4" opacity="0.8"/>
<line x1="462" y1="352" x2="612" y2="352" stroke="#D4921A" stroke-width="0.6" opacity="0.48"/>
<circle cx="460" cy="352" r="2.2" fill="#1DC9A4" opacity="0.8"/>
<text x="340" y="369" text-anchor="middle" font-family="var(--font-mono)" font-size="10" font-weight="400" fill="#5A7080" letter-spacing="3">SECURITY · RESEARCH · EXPLOITATION</text>
<text x="57" y="378" font-family="var(--font-mono)" font-size="9" fill="#1DC9A4" opacity="0.5" letter-spacing="1">v4.2.0</text>
<text x="623" y="378" text-anchor="end" font-family="var(--font-mono)" font-size="9" fill="#1DC9A4" opacity="0.5" letter-spacing="1">SL-01</text>
</svg>

After

Width:  |  Height:  |  Size: 5.9 KiB

View File

@@ -0,0 +1,299 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SETEC LABS - {% block title %}Manager{% endblock %}</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Courier New', monospace;
background: #0a0a0a;
color: #00ff41;
min-height: 100vh;
display: flex;
}
a { color: #00ff41; text-decoration: none; }
a:hover { color: #fff; }
/* Sidebar */
.sidebar {
width: 220px;
background: #111;
border-right: 1px solid #00ff41;
padding: 0;
position: fixed;
top: 0; left: 0; bottom: 0;
display: flex;
flex-direction: column;
}
.sidebar-logo {
padding: 10px 10px;
border-bottom: 1px solid #333;
text-align: center;
}
.sidebar-logo img { width: 100%; max-width: 200px; height: auto; }
.sidebar nav { padding: 10px 0; flex: 1; overflow-y: auto; }
.sidebar nav a {
display: block;
padding: 10px 20px;
border-bottom: 1px solid #1a1a1a;
transition: background 0.2s;
}
.sidebar nav a:hover, .sidebar nav a.active {
background: #1a2a1a;
color: #fff;
}
.sidebar nav a .icon { margin-right: 8px; }
.sidebar-bottom {
border-top: 1px solid #333;
flex-shrink: 0;
}
.sidebar-bottom a {
display: block;
padding: 10px 20px;
border-bottom: 1px solid #1a1a1a;
transition: background 0.2s;
color: #00ff41;
text-decoration: none;
}
.sidebar-bottom a:hover, .sidebar-bottom a.active {
background: #1a2a1a;
color: #fff;
}
.sidebar-bottom a .icon { margin-right: 8px; }
.sidebar .ver {
text-align: center;
font-size: 10px;
color: #555;
padding: 8px 0;
}
/* Main content */
.main {
margin-left: 220px;
flex: 1;
padding: 20px 30px;
min-height: 100vh;
}
h1 {
font-size: 18px;
border-bottom: 1px solid #333;
padding-bottom: 8px;
margin-bottom: 20px;
}
h2 { font-size: 14px; margin: 15px 0 8px; color: #88ff88; }
/* Cards */
.card {
background: #111;
border: 1px solid #333;
padding: 15px;
margin-bottom: 15px;
}
.card-title {
font-size: 13px;
color: #88ff88;
margin-bottom: 10px;
border-bottom: 1px solid #222;
padding-bottom: 5px;
}
/* Buttons */
.btn {
background: #1a2a1a;
color: #00ff41;
border: 1px solid #00ff41;
padding: 6px 14px;
font-family: 'Courier New', monospace;
font-size: 12px;
cursor: pointer;
margin: 3px;
transition: all 0.2s;
}
.btn:hover { background: #00ff41; color: #000; }
.btn-danger { border-color: #ff4444; color: #ff4444; }
.btn-danger:hover { background: #ff4444; color: #000; }
.btn-warn { border-color: #ffaa00; color: #ffaa00; }
.btn-warn:hover { background: #ffaa00; color: #000; }
/* Output */
.output {
background: #000;
border: 1px solid #333;
padding: 12px;
font-size: 12px;
white-space: pre-wrap;
word-break: break-all;
max-height: 500px;
overflow-y: auto;
color: #00ff41;
margin-top: 10px;
}
.output .err { color: #ff4444; }
.output .info { color: #888; }
/* Forms */
input, select, textarea {
background: #000;
color: #00ff41;
border: 1px solid #333;
padding: 6px 10px;
font-family: 'Courier New', monospace;
font-size: 12px;
margin: 3px;
}
input:focus, textarea:focus { border-color: #00ff41; outline: none; }
label { font-size: 12px; color: #888; display: block; margin: 5px 3px 2px; }
/* Table */
table { width: 100%; border-collapse: collapse; font-size: 12px; }
th { text-align: left; color: #888; border-bottom: 1px solid #333; padding: 6px; }
td { padding: 6px; border-bottom: 1px solid #1a1a1a; }
tr:hover { background: #1a1a1a; }
/* Grid */
.grid { display: grid; gap: 15px; }
.grid-2 { grid-template-columns: 1fr 1fr; }
.grid-3 { grid-template-columns: 1fr 1fr 1fr; }
/* Status indicators */
.status-ok { color: #00ff41; }
.status-err { color: #ff4444; }
.status-warn { color: #ffaa00; }
/* Loading */
.loading::after {
content: '...';
animation: dots 1s steps(3) infinite;
}
@keyframes dots {
0% { content: '.'; }
33% { content: '..'; }
66% { content: '...'; }
}
.toolbar { margin-bottom: 15px; display: flex; gap: 5px; flex-wrap: wrap; }
</style>
</head>
<body>
<div class="sidebar">
<div class="sidebar-logo">
<img src="{{ url_for('static', filename='setec_labs_logo.svg') }}" alt="SETEC LABS">
</div>
<nav>
<a href="/" class="{% if request.endpoint == 'dashboard' %}active{% endif %}">
<span class="icon">[~]</span> Dashboard
</a>
<a href="/docker" class="{% if request.endpoint == 'docker_page' %}active{% endif %}">
<span class="icon">[#]</span> Docker
</a>
<a href="/dns" class="{% if request.endpoint == 'dns_page' %}active{% endif %}">
<span class="icon">[@]</span> DNS
</a>
<a href="/nginx" class="{% if request.endpoint == 'nginx_page' %}active{% endif %}">
<span class="icon">[>]</span> Nginx
</a>
<a href="/smtp" class="{% if request.endpoint == 'smtp_page' %}active{% endif %}">
<span class="icon">[*]</span> SMTP
</a>
<a href="/firewall" class="{% if request.endpoint == 'firewall_page' %}active{% endif %}">
<span class="icon">[|]</span> Firewall
</a>
<a href="/fail2ban" class="{% if request.endpoint == 'fail2ban_page' %}active{% endif %}">
<span class="icon">[!]</span> Fail2Ban
</a>
<a href="/frontpage" class="{% if request.endpoint == 'frontpage_page' %}active{% endif %}">
<span class="icon">[&lt;]</span> Front Page
</a>
<a href="/security" class="{% if request.endpoint == 'security_page' %}active{% endif %}">
<span class="icon">[&amp;]</span> Security
</a>
<a href="/detect" class="{% if request.endpoint == 'detect_page' %}active{% endif %}">
<span class="icon">[?]</span> Detect
</a>
<a href="/configs" class="{% if request.endpoint == 'configs_page' %}active{% endif %}">
<span class="icon">[=]</span> Configs
</a>
<a href="/files" class="{% if request.endpoint == 'files_page' %}active{% endif %}">
<span class="icon">[/]</span> Files
</a>
<a href="/terminal" class="{% if request.endpoint == 'terminal_page' %}active{% endif %}">
<span class="icon">[$]</span> Terminal
</a>
<a href="/settings" class="{% if request.endpoint == 'settings_page' %}active{% endif %}">
<span class="icon">[%]</span> Settings
</a>
</nav>
<div class="sidebar-bottom">
<a href="/docs" class="{% if request.endpoint == 'docs_page' %}active{% endif %}">
<span class="icon">[^]</span> Docs
</a>
<a href="/wizard" class="{% if request.endpoint == 'wizard_page' %}active{% endif %}">
<span class="icon">[+]</span> Setup Wizard
</a>
<div class="ver">setec-mgr v2.0</div>
</div>
</div>
<div class="main">
{% block content %}{% endblock %}
</div>
<script>
async function api(url, opts = {}) {
const el = document.getElementById('output');
if (el) el.innerHTML = '<span class="info">Loading...</span>';
try {
const r = await fetch(url, opts);
const j = await r.json();
return j;
} catch (e) {
if (el) el.innerHTML = '<span class="err">Error: ' + e.message + '</span>';
return {ok: false, error: e.message};
}
}
async function apiGet(url) { return api(url); }
async function apiPost(url, body = {}) {
return api(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
});
}
async function apiDelete(url, body = {}) {
return api(url, {
method: 'DELETE',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
});
}
function showResult(res, elId = 'output') {
const el = document.getElementById(elId);
if (!el) return;
if (!res.ok) {
el.innerHTML = '<span class="err">ERROR: ' + (res.error || 'Unknown error') + '</span>';
return;
}
const d = res.data;
if (typeof d === 'string') {
el.textContent = d;
} else if (d && d.stdout !== undefined) {
let html = '';
if (d.stdout) html += d.stdout;
if (d.stderr) html += '<span class="err">' + escHtml(d.stderr) + '</span>';
if (d.exit_code && d.exit_code !== 0) html += '\n<span class="err">[exit code: ' + d.exit_code + ']</span>';
el.innerHTML = html || '<span class="info">(no output)</span>';
} else {
el.textContent = JSON.stringify(d, null, 2);
}
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
}
</script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,222 @@
{% extends "base.html" %}
{% block title %}Config Editor{% endblock %}
{% block content %}
<h1>[=] Config Editor</h1>
<div class="grid grid-2">
<div>
<div class="card">
<div class="card-title">Quick Access</div>
<h2>Nginx</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/nginx/nginx.conf')">nginx.conf</button>
<button class="btn" onclick="listDir('/etc/nginx/sites-available/')">sites-available/</button>
<button class="btn" onclick="listDir('/etc/nginx/conf.d/')">conf.d/</button>
<button class="btn" onclick="listDir('/etc/nginx/snippets/')">snippets/</button>
</div>
<h2>GitLab</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/gitlab/gitlab.rb')">gitlab.rb</button>
</div>
<h2>Gitea</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/gitea/app.ini')">app.ini (etc)</button>
<button class="btn" onclick="loadConfig('/var/lib/gitea/custom/conf/app.ini')">app.ini (custom)</button>
</div>
<h2>SSH</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/ssh/sshd_config')">sshd_config</button>
<button class="btn" onclick="loadConfig('/etc/ssh/ssh_config')">ssh_config</button>
</div>
<h2>Mail</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/postfix/main.cf')">postfix main.cf</button>
<button class="btn" onclick="loadConfig('/etc/postfix/master.cf')">postfix master.cf</button>
<button class="btn" onclick="loadConfig('/etc/opendkim.conf')">opendkim.conf</button>
</div>
<h2>Security</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/fail2ban/jail.local')">fail2ban jail</button>
<button class="btn" onclick="loadConfig('/etc/ufw/ufw.conf')">ufw.conf</button>
</div>
<h2>Docker</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/docker/daemon.json')">daemon.json</button>
<button class="btn" onclick="loadConfig('/opt/seteclabs/docker-compose.yml')">docker-compose.yml</button>
</div>
<h2>System</h2>
<div class="toolbar">
<button class="btn" onclick="loadConfig('/etc/hosts')">hosts</button>
<button class="btn" onclick="loadConfig('/etc/hostname')">hostname</button>
<button class="btn" onclick="loadConfig('/etc/resolv.conf')">resolv.conf</button>
<button class="btn" onclick="loadConfig('/etc/fstab')">fstab</button>
<button class="btn" onclick="loadConfig('/etc/crontab')">crontab</button>
</div>
<h2>Custom Path</h2>
<div class="toolbar">
<input type="text" id="custom-path" placeholder="/etc/some/config.conf" style="width:300px">
<button class="btn" onclick="loadConfig(document.getElementById('custom-path').value)">Load</button>
</div>
</div>
<div class="card">
<div class="card-title">Directory Browser</div>
<div class="output" id="dir-list" style="max-height:300px"><span class="info">Click a directory button above</span></div>
</div>
</div>
<div>
<div class="card">
<div class="card-title">
Editor: <span id="current-file" style="color:#ffaa00">none</span>
</div>
<textarea id="config-editor" rows="30" style="width:100%;resize:vertical;tab-size:4" spellcheck="false"></textarea>
<div class="toolbar" style="margin-top:5px">
<button class="btn" onclick="saveConfig()">Save</button>
<button class="btn" onclick="testConfig()">Test Nginx</button>
<button class="btn" onclick="showDiff()">Diff vs Backup</button>
<button class="btn" onclick="showBackups()">List Backups</button>
<button class="btn btn-warn" onclick="reloadSvc()">Reload Service</button>
</div>
<div style="margin-top:5px">
<label>Reload service after save:</label>
<select id="reload-svc">
<option value="">-- none --</option>
<option value="nginx">nginx</option>
<option value="sshd">sshd</option>
<option value="postfix">postfix</option>
<option value="opendkim">opendkim</option>
<option value="fail2ban">fail2ban</option>
<option value="docker">docker</option>
<option value="redis">redis</option>
<option value="postgresql">postgresql</option>
<option value="mysql">mysql</option>
<option value="dovecot">dovecot</option>
<option value="grafana-server">grafana</option>
<option value="prometheus">prometheus</option>
</select>
</div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready. Click a config file to edit.</span></div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentFile = '';
// Check URL params for ?file=
const params = new URLSearchParams(window.location.search);
if (params.get('file')) {
loadConfig(params.get('file'));
}
async function loadConfig(path) {
if (!path) return;
currentFile = path;
document.getElementById('current-file').textContent = path;
document.getElementById('config-editor').value = 'Loading...';
const res = await apiGet('/api/configs/read?path=' + encodeURIComponent(path));
if (res.ok) {
document.getElementById('config-editor').value = res.data.stdout || '';
document.getElementById('output').innerHTML = '<span class="info">Loaded ' + escHtml(path) + ' (' + (res.data.stdout||'').split('\n').length + ' lines)</span>';
} else {
document.getElementById('config-editor').value = '';
document.getElementById('output').innerHTML = '<span class="err">' + escHtml(res.error || 'Failed to read file') + '</span>';
}
// Auto-detect reload service
if (path.includes('nginx')) document.getElementById('reload-svc').value = 'nginx';
else if (path.includes('postfix')) document.getElementById('reload-svc').value = 'postfix';
else if (path.includes('sshd') || path.includes('/ssh/')) document.getElementById('reload-svc').value = 'sshd';
else if (path.includes('opendkim')) document.getElementById('reload-svc').value = 'opendkim';
else if (path.includes('fail2ban')) document.getElementById('reload-svc').value = 'fail2ban';
else if (path.includes('docker')) document.getElementById('reload-svc').value = 'docker';
}
async function saveConfig() {
if (!currentFile) { alert('No file loaded'); return; }
if (!confirm('Save changes to ' + currentFile + '?\nA backup will be created automatically.')) return;
const content = document.getElementById('config-editor').value;
const res = await apiPost('/api/configs/write', {path: currentFile, content: content});
if (res.ok) {
document.getElementById('output').innerHTML = '<span class="status-ok">Saved ' + escHtml(currentFile) + ' (backup created)</span>';
// Auto-reload service if selected
const svc = document.getElementById('reload-svc').value;
if (svc) {
const r2 = await apiPost('/api/configs/reload-service', {service: svc});
showResult(r2);
}
} else {
showResult(res);
}
}
async function testConfig() {
const res = await apiPost('/api/configs/test-nginx');
showResult(res);
}
async function showDiff() {
if (!currentFile) return;
const res = await apiPost('/api/configs/diff', {path: currentFile});
showResult(res);
}
async function showBackups() {
if (!currentFile) return;
const res = await apiGet('/api/configs/backups?path=' + encodeURIComponent(currentFile));
showResult(res);
}
async function reloadSvc() {
const svc = document.getElementById('reload-svc').value;
if (!svc) { alert('Select a service to reload'); return; }
const res = await apiPost('/api/configs/reload-service', {service: svc});
showResult(res);
}
async function listDir(path) {
const res = await apiGet('/api/files/list?path=' + encodeURIComponent(path));
const el = document.getElementById('dir-list');
if (!res.ok) { el.innerHTML = '<span class="err">' + res.error + '</span>'; return; }
const lines = (res.data.stdout || '').trim().split('\n');
let html = '';
for (const line of lines) {
const parts = line.trim().split(/\s+/);
if (parts.length < 9) continue;
const fname = parts.slice(8).join(' ');
if (fname === '.' || fname === '..') continue;
const isDir = line.startsWith('d');
const fullPath = path.replace(/\/$/, '') + '/' + fname;
if (isDir) {
html += `<a href="#" onclick="listDir('${fullPath}/');return false" style="color:#ffaa00">${fname}/</a>\n`;
} else {
html += `<a href="#" onclick="loadConfig('${fullPath}');return false">${fname}</a>\n`;
}
}
el.innerHTML = html || '<span class="info">(empty)</span>';
}
// Handle tab key in editor
document.getElementById('config-editor').addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
const start = this.selectionStart;
const end = this.selectionEnd;
this.value = this.value.substring(0, start) + ' ' + this.value.substring(end);
this.selectionStart = this.selectionEnd = start + 4;
}
// Ctrl+S to save
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
saveConfig();
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<h1>[~] Dashboard</h1>
<div class="toolbar">
<button class="btn" onclick="loadStatus()">Refresh Status</button>
<button class="btn" onclick="loadDomains()">Check Domains</button>
<button class="btn" onclick="deploySite()">Deploy Site</button>
<button class="btn" onclick="testSSH()">Test SSH</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Server Status</div>
<div class="output" id="status-output"><span class="info">Click "Refresh Status" to load</span></div>
</div>
<div class="card">
<div class="card-title">Domain Status</div>
<div class="output" id="domain-output"><span class="info">Click "Check Domains" to load</span></div>
</div>
</div>
<div class="card">
<div class="card-title">Quick Actions</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function loadStatus() {
const res = await apiGet('/api/status');
showResult(res, 'status-output');
}
async function loadDomains() {
const res = await apiGet('/api/status/domains');
showResult(res, 'domain-output');
}
async function deploySite() {
if (!confirm('Deploy site/ to VPS?')) return;
document.getElementById('output').innerHTML = '<span class="info">Deploying...</span>';
const res = await apiPost('/api/deploy/site');
showResult(res);
}
async function testSSH() {
const res = await apiGet('/api/ssh/test');
showResult(res);
}
// Auto-load on page load
loadStatus();
loadDomains();
</script>
{% endblock %}

View File

@@ -0,0 +1,140 @@
{% extends "base.html" %}
{% block title %}Service Detection{% endblock %}
{% block content %}
<h1>[?] Service Detection</h1>
<div class="toolbar">
<button class="btn" onclick="runScan()">Scan Server</button>
<button class="btn" onclick="browseAll()">Browse All 200+ Services</button>
</div>
<div id="scan-results"></div>
<div class="card" id="browse-panel" style="display:none">
<div class="card-title">Service Database ({{count}} services)</div>
<input type="text" id="svc-search" placeholder="Filter services..." style="width:100%;margin-bottom:10px" oninput="filterServices()">
<div id="all-services" style="max-height:600px;overflow-y:auto"></div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Click "Scan Server" to detect installed services</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
let allServices = {};
async function runScan() {
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="card-title">Scanning...</div><div class="output"><span class="info">Checking processes, ports, packages, configs, docker, systemd...</span></div></div>';
const res = await apiGet('/api/detect/scan');
if (!res.ok) {
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="output"><span class="err">'+res.error+'</span></div></div>';
return;
}
const services = res.data;
if (!services.length) {
document.getElementById('scan-results').innerHTML = '<div class="card"><div class="output"><span class="info">No services detected (or SSH failed)</span></div></div>';
return;
}
// Group by category
const groups = {};
services.forEach(s => {
const cat = s.category;
if (!groups[cat]) groups[cat] = [];
groups[cat].push(s);
});
let html = '<div class="card"><div class="card-title">Detected Services (' + services.length + ' found)</div></div>';
for (const [cat, svcs] of Object.entries(groups)) {
html += '<div class="card"><div class="card-title">' + escHtml(cat) + ' (' + svcs.length + ')</div>';
html += '<table><thead><tr><th>Service</th><th>Confidence</th><th>Evidence</th><th>Configs</th><th>Actions</th></tr></thead><tbody>';
for (const s of svcs) {
const conf = s.score >= 5 ? 'status-ok' : s.score >= 3 ? 'status-warn' : 'status-err';
const confLabel = s.score >= 5 ? 'HIGH' : s.score >= 3 ? 'MED' : 'LOW';
const configLinks = s.configs.filter(c => c && !c.endsWith('/')).map(c =>
`<a href="#" onclick="editConfig('${c}');return false" style="font-size:11px">${c.split('/').pop()}</a>`
).join(', ');
const configDirs = s.configs.filter(c => c && c.endsWith('/')).map(c =>
`<a href="#" onclick="browseConfig('${c}');return false" style="font-size:11px;color:#888">${c}</a>`
).join(', ');
html += '<tr>';
html += '<td><strong>' + escHtml(s.name) + '</strong></td>';
html += '<td class="' + conf + '">' + confLabel + ' (' + s.score + ')</td>';
html += '<td style="font-size:11px;color:#888">' + s.evidence.map(escHtml).join(', ') + '</td>';
html += '<td>' + configLinks + (configDirs ? '<br>' + configDirs : '') + '</td>';
html += '<td>';
if (s.ports.length) html += '<span style="color:#555">ports: ' + s.ports.join(',') + '</span> ';
html += '</td>';
html += '</tr>';
}
html += '</tbody></table></div>';
}
document.getElementById('scan-results').innerHTML = html;
}
function editConfig(path) {
window.location.href = '/configs?file=' + encodeURIComponent(path);
}
function browseConfig(path) {
window.location.href = '/files?path=' + encodeURIComponent(path);
}
async function browseAll() {
const panel = document.getElementById('browse-panel');
panel.style.display = '';
if (Object.keys(allServices).length) return;
const res = await apiGet('/api/detect/all-services');
if (!res.ok) return;
allServices = res.data;
renderAllServices(allServices);
}
function renderAllServices(data) {
let html = '';
let total = 0;
for (const [cat, svcs] of Object.entries(data)) {
total += svcs.length;
html += '<h2 style="margin-top:10px">' + escHtml(cat) + ' (' + svcs.length + ')</h2>';
html += '<table><thead><tr><th>Service</th><th>Ports</th><th>Packages</th><th>Config Files</th></tr></thead><tbody>';
for (const s of svcs) {
html += '<tr>';
html += '<td>' + escHtml(s.name) + '</td>';
html += '<td style="color:#888">' + (s.ports.length ? s.ports.join(', ') : '-') + '</td>';
html += '<td style="font-size:11px;color:#888">' + (s.packages.length ? s.packages.join(', ') : '-') + '</td>';
html += '<td style="font-size:11px">' + (s.configs.length ? s.configs.map(c =>
'<a href="#" onclick="editConfig(\'' + c + '\');return false">' + c + '</a>'
).join('<br>') : '-') + '</td>';
html += '</tr>';
}
html += '</tbody></table>';
}
document.getElementById('all-services').innerHTML = html;
document.querySelector('#browse-panel .card-title').textContent = 'Service Database (' + total + ' services)';
}
function filterServices() {
const q = document.getElementById('svc-search').value.toLowerCase();
if (!q) { renderAllServices(allServices); return; }
const filtered = {};
for (const [cat, svcs] of Object.entries(allServices)) {
const matches = svcs.filter(s =>
s.name.toLowerCase().includes(q) ||
cat.toLowerCase().includes(q) ||
s.packages.some(p => p.includes(q)) ||
s.configs.some(c => c.includes(q))
);
if (matches.length) filtered[cat] = matches;
}
renderAllServices(filtered);
}
</script>
{% endblock %}

View File

@@ -0,0 +1,101 @@
{% extends "base.html" %}
{% block title %}DNS{% endblock %}
{% block content %}
<h1>[@] DNS Management</h1>
<div class="toolbar">
<button class="btn" onclick="loadRecords()">Refresh Records</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">DNS Records (Hostinger)</div>
<div id="dns-records" class="output" style="max-height:600px"><span class="info">Loading...</span></div>
</div>
<div>
<div class="card">
<div class="card-title">Add Record</div>
<label>Type</label>
<select id="dns-type">
<option value="A">A</option>
<option value="TXT">TXT</option>
</select>
<label>Name (subdomain or @ for root)</label>
<input type="text" id="dns-name" placeholder="e.g. app" style="width:100%">
<label>Value</label>
<input type="text" id="dns-value" placeholder="e.g. 31.220.20.55" style="width:100%">
<br><br>
<button class="btn" onclick="addRecord()">Add Record</button>
</div>
<div class="card">
<div class="card-title">Delete Record</div>
<label>Record ID</label>
<input type="text" id="dns-del-id" placeholder="record ID from list" style="width:100%">
<br><br>
<button class="btn btn-danger" onclick="deleteRecord()">Delete Record</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function loadRecords() {
const el = document.getElementById('dns-records');
el.innerHTML = '<span class="info">Loading...</span>';
const res = await apiGet('/api/dns/records');
if (!res.ok) { el.innerHTML = '<span class="err">'+res.error+'</span>'; return; }
const records = Array.isArray(res.data) ? res.data : (res.data.records || [res.data]);
let html = '';
for (const r of records) {
if (Array.isArray(r)) {
for (const rec of r) {
html += formatRecord(rec);
}
} else {
html += formatRecord(r);
}
}
el.innerHTML = html || '<span class="info">No records found</span>';
}
function formatRecord(r) {
const id = r.id || r.record_id || '?';
const type = r.type || '?';
const name = r.name || r.host || '?';
const val = r.content || r.value || r.address || '?';
const ttl = r.ttl || '';
return `<div style="margin-bottom:4px;border-bottom:1px solid #222;padding-bottom:4px">` +
`<span style="color:#888">[${id}]</span> ` +
`<span style="color:#ffaa00">${type}</span> ` +
`${name}${val} ` +
`<span style="color:#555">TTL:${ttl}</span></div>`;
}
async function addRecord() {
const type = document.getElementById('dns-type').value;
const name = document.getElementById('dns-name').value;
const value = document.getElementById('dns-value').value;
if (!name || !value) { alert('Fill in name and value'); return; }
const res = await apiPost('/api/dns/add', {type, name, value});
showResult(res);
loadRecords();
}
async function deleteRecord() {
const id = document.getElementById('dns-del-id').value;
if (!id) return;
if (!confirm('Delete record '+id+'?')) return;
const res = await apiDelete('/api/dns/delete/'+id);
showResult(res);
loadRecords();
}
loadRecords();
</script>
{% endblock %}

View File

@@ -0,0 +1,334 @@
{% extends "base.html" %}
{% block title %}Docker{% endblock %}
{% block content %}
<h1>[#] Docker Management</h1>
<div class="toolbar">
<button class="btn" onclick="loadContainers()">Refresh</button>
<button class="btn" onclick="composeAction('up')">Compose Up</button>
<button class="btn btn-warn" onclick="composeAction('down')">Compose Down</button>
<button class="btn" onclick="composeAction('pull')">Compose Pull</button>
<button class="btn" onclick="showTab('containers')">Containers</button>
<button class="btn" onclick="showTab('store')">App Store</button>
<button class="btn" onclick="showTab('install')">Install from Git/URL</button>
</div>
<!-- TAB: Containers -->
<div id="tab-containers">
<div class="card">
<div class="card-title">Running Containers</div>
<table>
<thead><tr><th>Name</th><th>Image</th><th>Status</th><th>Ports / Web UI</th><th>Actions</th></tr></thead>
<tbody id="container-list"><tr><td colspan="5" class="info">Loading...</td></tr></tbody>
</table>
</div>
<div class="card">
<div class="card-title">Container Logs</div>
<div class="toolbar">
<input type="text" id="log-container" placeholder="container name" style="width:200px">
<input type="number" id="log-lines" value="50" style="width:80px">
<button class="btn" onclick="loadLogs()">View Logs</button>
</div>
<div class="output" id="output"><span class="info">Select a container to view logs</span></div>
</div>
</div>
<!-- TAB: App Store -->
<div id="tab-store" style="display:none">
<div class="card">
<div class="card-title">Docker App Store</div>
<div class="toolbar">
<input type="text" id="store-search" placeholder="Search apps..." style="width:300px" oninput="filterStore()">
<select id="store-cat" onchange="filterStore()">
<option value="">All Categories</option>
</select>
</div>
<div id="store-grid" class="grid grid-2" style="margin-top:10px"></div>
</div>
<div class="card">
<div class="card-title">Install Output</div>
<div class="output" id="store-output"><span class="info">Select an app to install</span></div>
</div>
</div>
<!-- TAB: Install from Git/URL -->
<div id="tab-install" style="display:none">
<div class="grid grid-2">
<div class="card">
<div class="card-title">Install from GitHub / Git Repo</div>
<label>Git Repository URL</label>
<input type="text" id="git-repo" placeholder="https://github.com/user/repo.git" style="width:100%">
<label>App Name (optional, auto-detected from URL)</label>
<input type="text" id="git-name" placeholder="my-app" style="width:100%">
<br><br>
<button class="btn" onclick="installGit()">Clone & Deploy</button>
<p style="font-size:11px;color:#555;margin-top:8px">
Clones the repo to /opt/seteclabs/&lt;name&gt; and runs docker compose up if a compose file is found.
</p>
</div>
<div class="card">
<div class="card-title">Install from URL (tar.gz, zip, binary)</div>
<label>Download URL</label>
<input type="text" id="url-url" placeholder="https://example.com/app.tar.gz" style="width:100%">
<label>App Name</label>
<input type="text" id="url-name" placeholder="my-app" style="width:100%">
<br><br>
<button class="btn" onclick="installUrl()">Download & Deploy</button>
<p style="font-size:11px;color:#555;margin-top:8px">
Downloads to /opt/seteclabs/&lt;name&gt;/, extracts if archive, runs docker compose if found.
</p>
</div>
</div>
<div class="card">
<div class="card-title">Install from Docker Compose (paste YAML)</div>
<label>App Name</label>
<input type="text" id="compose-name" placeholder="my-app" style="width:300px">
<label>Docker Compose YAML</label>
<textarea id="compose-yaml" rows="12" style="width:100%;tab-size:2" placeholder="services:
myapp:
image: myimage:latest
ports:
- '8080:80'"></textarea>
<br>
<button class="btn" onclick="installCompose()">Deploy Compose</button>
</div>
<div class="card">
<div class="card-title">Install Output</div>
<div class="output" id="install-output"><span class="info">Ready.</span></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const VPS_IP = '31.220.20.55';
let storeData = [];
// Known container -> web UI port mappings
const UI_MAP = {
'gitea': {port: 3000, proto: 'https', host: 'repo.seteclabs.io', path: '/'},
'gitlab': {port: 8080, proto: 'https', host: 'git.seteclabs.io', path: '/'},
'listmonk': {port: 9000, proto: 'https', host: 'lists.seteclabs.io', path: '/'},
'portainer': {port: 9443, proto: 'https', path: '/'},
'uptime-kuma': {port: 3001, path: '/'},
'grafana': {port: 3002, path: '/'},
'netdata': {port: 19999, path: '/'},
'nginx-proxy-manager': {port: 81, path: '/'},
'dockge': {port: 5001, path: '/'},
'nextcloud': {port: 8081, path: '/'},
'filebrowser': {port: 8082, path: '/'},
'vaultwarden': {port: 8083, path: '/'},
'wikijs': {port: 3003, path: '/'},
'n8n': {port: 5678, path: '/'},
'nodered': {port: 1880, path: '/'},
'privatebin': {port: 8090, path: '/'},
'it-tools': {port: 8091, path: '/'},
'cyberchef': {port: 8092, path: '/'},
'jellyfin': {port: 8096, path: '/'},
'homer': {port: 8094, path: '/'},
'homarr': {port: 7575, path: '/'},
'wg-easy': {port: 51821, path: '/'},
'woodpecker': {port: 8000, path: '/'},
'plausible': {port: 8088, path: '/'},
'umami': {port: 8089, path: '/'},
'gotify': {port: 8085, path: '/'},
'ntfy': {port: 8086, path: '/'},
'cockpit': {port: 9090, path: '/'},
'minio': {port: 9011, path: '/'},
'bookstack': {port: 6875, path: '/'},
'stirling-pdf': {port: 8093, path: '/'},
};
function getWebLink(name, ports) {
// Check known map first
const known = UI_MAP[name];
if (known) {
const host = known.host || VPS_IP;
const proto = known.proto || 'http';
const port = (proto === 'https' && (known.port === 443 || known.host)) ? '' : ':' + known.port;
return `${proto}://${host}${port}${known.path}`;
}
// Try to extract from port mapping string
if (ports) {
const match = ports.match(/0\.0\.0\.0:(\d+)/);
if (match) return `http://${VPS_IP}:${match[1]}`;
const match2 = ports.match(/:::(\d+)/);
if (match2) return `http://${VPS_IP}:${match2[1]}`;
}
return null;
}
// Tab switching
function showTab(tab) {
document.getElementById('tab-containers').style.display = tab === 'containers' ? '' : 'none';
document.getElementById('tab-store').style.display = tab === 'store' ? '' : 'none';
document.getElementById('tab-install').style.display = tab === 'install' ? '' : 'none';
if (tab === 'store' && !storeData.length) loadStore();
}
// ── Containers ──
async function loadContainers() {
const res = await apiGet('/api/docker/list');
const tbody = document.getElementById('container-list');
if (!res.ok) { tbody.innerHTML = '<tr><td colspan="5" class="err">'+res.error+'</td></tr>'; return; }
const lines = res.data.stdout.trim().split('\n').filter(l => l);
if (!lines.length) { tbody.innerHTML = '<tr><td colspan="5" class="info">No containers</td></tr>'; return; }
tbody.innerHTML = lines.map(line => {
const [id, name, image, status, ports] = line.split('|');
const running = status && status.includes('Up');
const webLink = getWebLink(name, ports);
const portDisplay = webLink
? `<a href="${webLink}" target="_blank" style="color:#00ff41">${webLink}</a>`
: `<span style="color:#888;font-size:11px">${ports||'none'}</span>`;
return `<tr>
<td><strong>${name||''}</strong></td>
<td style="color:#888;font-size:11px">${image||''}</td>
<td class="${running?'status-ok':'status-err'}">${status||''}</td>
<td>${portDisplay}</td>
<td>
<button class="btn" onclick="dockerAction('restart','${name}')">restart</button>
${running
? `<button class="btn btn-warn" onclick="dockerAction('stop','${name}')">stop</button>`
: `<button class="btn" onclick="dockerAction('start','${name}')">start</button>`}
<button class="btn" onclick="viewLogs('${name}')">logs</button>
</td>
</tr>`;
}).join('');
}
async function dockerAction(action, name) {
const res = await apiPost('/api/docker/'+action+'/'+name);
showResult(res);
setTimeout(loadContainers, 1000);
}
async function composeAction(action) {
document.getElementById('output').innerHTML = '<span class="info">Running compose '+action+'...</span>';
const res = await apiPost('/api/docker/compose/'+action);
showResult(res);
loadContainers();
}
function viewLogs(name) {
showTab('containers');
document.getElementById('log-container').value = name;
loadLogs();
}
async function loadLogs() {
const name = document.getElementById('log-container').value;
const lines = document.getElementById('log-lines').value;
if (!name) return;
const res = await apiGet('/api/docker/logs/'+name+'?lines='+lines);
showResult(res);
}
// ── Store ──
async function loadStore() {
const res = await apiGet('/api/docker/store');
if (!res.ok) return;
storeData = res.data;
const catSelect = document.getElementById('store-cat');
const cats = res.categories || [...new Set(storeData.map(a => a.cat))];
cats.forEach(c => {
const opt = document.createElement('option');
opt.value = c; opt.textContent = c;
catSelect.appendChild(opt);
});
renderStore(storeData);
}
function filterStore() {
const q = document.getElementById('store-search').value.toLowerCase();
const cat = document.getElementById('store-cat').value;
const filtered = storeData.filter(a =>
(!q || a.name.toLowerCase().includes(q) || a.desc.toLowerCase().includes(q)) &&
(!cat || a.cat === cat)
);
renderStore(filtered);
}
function renderStore(apps) {
const grid = document.getElementById('store-grid');
grid.innerHTML = apps.map(a => {
const ports = Object.entries(a.ports).map(([p, l]) => `${p} (${l})`).join(', ');
return `<div class="card" style="padding:12px">
<div style="display:flex;justify-content:space-between;align-items:center">
<div>
<strong style="color:#00ff41;font-size:14px">${escHtml(a.name)}</strong>
<span style="color:#555;font-size:11px;margin-left:8px">${escHtml(a.cat)}</span>
</div>
<button class="btn" onclick="installStore('${escHtml(a.name)}')">Install</button>
</div>
<div style="color:#aaa;font-size:12px;margin:6px 0">${escHtml(a.desc)}</div>
<div style="font-size:11px;color:#555">
Ports: ${ports}<br>
Image: ${escHtml(a.image)}
</div>
${a.notes ? `<div style="font-size:11px;color:#ffaa00;margin-top:4px">${escHtml(a.notes)}</div>` : ''}
</div>`;
}).join('');
if (!apps.length) grid.innerHTML = '<div class="info" style="padding:20px">No apps match your search</div>';
}
async function installStore(name) {
if (!confirm('Install ' + name + '?\nThis will add it to docker-compose.yml and start it.')) return;
const out = document.getElementById('store-output');
out.innerHTML = '<span class="info">Installing ' + name + '...</span>';
const res = await apiPost('/api/docker/store/install', {name});
showResult(res, 'store-output');
loadContainers();
}
// ── Git/URL Install ──
async function installGit() {
const repo = document.getElementById('git-repo').value.trim();
const name = document.getElementById('git-name').value.trim();
if (!repo) { alert('Enter a git repo URL'); return; }
const out = document.getElementById('install-output');
out.innerHTML = '<span class="info">Cloning and deploying...</span>';
const res = await apiPost('/api/docker/install-git', {repo, name});
showResult(res, 'install-output');
loadContainers();
}
async function installUrl() {
const url = document.getElementById('url-url').value.trim();
const name = document.getElementById('url-name').value.trim();
if (!url || !name) { alert('Enter URL and app name'); return; }
const out = document.getElementById('install-output');
out.innerHTML = '<span class="info">Downloading and deploying...</span>';
const res = await apiPost('/api/docker/install-url', {url, name});
showResult(res, 'install-output');
loadContainers();
}
async function installCompose() {
const name = document.getElementById('compose-name').value.trim();
const compose = document.getElementById('compose-yaml').value;
if (!name || !compose) { alert('Enter app name and compose YAML'); return; }
const out = document.getElementById('install-output');
out.innerHTML = '<span class="info">Deploying compose stack...</span>';
const res = await apiPost('/api/docker/install-compose', {name, compose});
showResult(res, 'install-output');
loadContainers();
}
// Tab key in compose textarea
document.getElementById('compose-yaml').addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
const s = this.selectionStart;
this.value = this.value.substring(0, s) + ' ' + this.value.substring(this.selectionEnd);
this.selectionStart = this.selectionEnd = s + 2;
}
});
loadContainers();
</script>
{% endblock %}

View File

@@ -0,0 +1,609 @@
{% extends "base.html" %}
{% block title %}Documentation{% endblock %}
{% block content %}
<h1>[^] Documentation</h1>
<div class="toolbar">
<button class="btn" onclick="showDoc('manual')">User Manual</button>
<button class="btn" onclick="showDoc('hostlinks')">Host API / SSH Links</button>
<button class="btn" onclick="showDoc('troubleshoot')">Troubleshooting Guide</button>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- USER MANUAL -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="doc-manual" class="card">
<div class="card-title">User Manual</div>
<div style="font-size:12px;line-height:1.7;color:#ccc">
<h2>1. Introduction</h2>
<p>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 &mdash; all from your browser.</p>
<p style="color:#888;font-size:11px">Requirements: A Linux VPS (Debian/Ubuntu recommended), SSH key access, Python 3.10+</p>
<h2>2. Installation</h2>
<div style="background:#000;padding:10px;border:1px solid #333;margin:8px 0">
<span style="color:#888"># Clone the repository</span><br>
<span style="color:#00ff41">git clone https://repo.seteclabs.io/setec/setec-mgr.git</span><br>
<span style="color:#00ff41">cd setec-mgr</span><br><br>
<span style="color:#888"># Install dependencies</span><br>
<span style="color:#00ff41">pip install -r requirements.txt</span><br><br>
<span style="color:#888"># Start the manager</span><br>
<span style="color:#00ff41">python app.py</span><br><br>
<span style="color:#888"># Open in browser</span><br>
<span style="color:#00ff41">http://localhost:5000</span>
</div>
<h2>3. Initial Setup (Setup Wizard)</h2>
<p>On first launch, click <strong style="color:#00ff41">Setup Wizard</strong> in the sidebar. The wizard walks you through:</p>
<ol style="margin-left:15px;line-height:2">
<li><strong style="color:#88ff88">Terms of Service</strong> &mdash; Read and accept the disclaimer.</li>
<li><strong style="color:#88ff88">SSH Keys</strong> &mdash; Select existing keys or generate new ones with host-specific guidance.</li>
<li><strong style="color:#88ff88">VPS Connection</strong> &mdash; Enter your server IP, SSH username, port (2222 recommended), and key path.</li>
<li><strong style="color:#88ff88">DNS API</strong> &mdash; Select your hosting provider, enter your domain and API key.</li>
<li><strong style="color:#88ff88">Paths</strong> &mdash; Set web root and Docker Compose file location.</li>
<li><strong style="color:#88ff88">Connection Test</strong> &mdash; Verify SSH and API connectivity.</li>
</ol>
<p style="color:#888;font-size:11px">You can re-run the wizard at any time. All settings are also editable from the Settings page.</p>
<h2>4. Dashboard</h2>
<p>The Dashboard shows a real-time overview of your server:</p>
<ul style="margin-left:15px;line-height:2">
<li><strong>System Info</strong> &mdash; Hostname, OS, kernel, uptime</li>
<li><strong>Resource Usage</strong> &mdash; CPU, RAM, disk, swap</li>
<li><strong>Network</strong> &mdash; Active connections, listening ports</li>
<li><strong>Services</strong> &mdash; Status of key services (nginx, sshd, etc.)</li>
</ul>
<h2>5. Docker Management</h2>
<p>Manage containers, images, volumes, and networks. Start/stop/restart containers, view logs,
pull images, and manage your Docker Compose stack.</p>
<div style="background:#000;padding:8px;border:1px solid #333;margin:8px 0;font-size:11px;color:#888">
Note: SETEC runs services natively with systemd by default. Docker management is provided for
users who have containerized workloads.
</div>
<h2>6. DNS Management</h2>
<p>View, add, and delete DNS records through your hosting provider's API. Supports:</p>
<ul style="margin-left:15px;line-height:2">
<li>A, AAAA, CNAME, MX, TXT, NS records</li>
<li>10 hosting providers (see Host API / SSH Links tab)</li>
<li>Fallback to <code style="color:#00ff41">dig</code> when API is unavailable</li>
</ul>
<h2>7. Nginx Management</h2>
<p>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).</p>
<h2>8. SMTP / Email</h2>
<p>Configure and manage mail services. View mail queue, check DKIM/SPF/DMARC records,
test email delivery, and manage Postfix configuration.</p>
<h2>9. Firewall</h2>
<p>The Firewall page (separate from Security) provides:</p>
<ul style="margin-left:15px;line-height:2">
<li><strong>Dashboard</strong> &mdash; Firewall activity overview and monitoring</li>
<li><strong>UFW</strong> &mdash; Simplified firewall rule management</li>
<li><strong>iptables</strong> &mdash; Advanced packet filtering rules</li>
<li><strong>nftables</strong> &mdash; Modern netfilter framework management</li>
<li><strong>firewalld</strong> &mdash; Zone-based firewall management</li>
<li><strong>CSF</strong> &mdash; ConfigServer Security &amp; Firewall</li>
<li><strong>Migration</strong> &mdash; Convert between UFW and iptables with one click</li>
</ul>
<h2>10. Fail2Ban</h2>
<p>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.</p>
<h2>11. Security Center</h2>
<p>The Security page is your central hub for hardening and monitoring:</p>
<p style="color:#ffaa00;margin-top:10px">Hardening Tools</p>
<ul style="margin-left:15px;line-height:2">
<li><strong>SSH Hardening</strong> &mdash; Disable root login, enforce key auth, change port</li>
<li><strong>Kernel Hardening</strong> &mdash; Sysctl tweaks for network and memory protection</li>
<li><strong>Auto Updates</strong> &mdash; Enable unattended-upgrades for security patches</li>
<li><strong>.sec Patch System</strong> &mdash; Apply SETEC-curated distro-specific security patches</li>
</ul>
<p style="color:#ffaa00;margin-top:10px">Security Applications (each with full management tab)</p>
<table style="margin:8px 0">
<tr><th>App</th><th>Purpose</th></tr>
<tr><td style="color:#00ff41">ClamAV</td><td>Antivirus scanning, quarantine management, scheduled scans</td></tr>
<tr><td style="color:#00ff41">rkhunter</td><td>Rootkit detection, file property checks</td></tr>
<tr><td style="color:#00ff41">chkrootkit</td><td>Alternative rootkit scanner with expert mode</td></tr>
<tr><td style="color:#00ff41">Lynis</td><td>Security auditing and hardening index scoring</td></tr>
<tr><td style="color:#00ff41">OSSEC</td><td>Host-based intrusion detection (HIDS), log monitoring, alerts</td></tr>
<tr><td style="color:#00ff41">ModSecurity</td><td>Web application firewall (WAF) for nginx, OWASP CRS rules</td></tr>
<tr><td style="color:#00ff41">AIDE</td><td>File integrity monitoring, baseline comparison</td></tr>
<tr><td style="color:#00ff41">Cowrie</td><td>SSH/Telnet honeypot for attacker monitoring</td></tr>
</table>
<p style="color:#888;font-size:11px">Each app tab provides: install/uninstall, status, configuration, scanning/auditing, logs, and scheduled tasks.</p>
<h2>12. Detect</h2>
<p>Server detection and fingerprinting. Identifies installed software, open ports,
running services, and potential security issues.</p>
<h2>13. Configs</h2>
<p>View and edit critical configuration files directly: sshd_config, nginx.conf,
jail.local, and other system configs with syntax-aware editing.</p>
<h2>14. Files</h2>
<p>Browse the server filesystem, view file contents, upload and download files,
manage permissions, and navigate directories.</p>
<h2>15. Terminal</h2>
<p>Direct SSH terminal access from the browser. Execute commands on your server
with full output display. Useful for tasks not covered by the GUI.</p>
<h2>16. Settings</h2>
<p>Configure all SETEC Manager settings:</p>
<ul style="margin-left:15px;line-height:2">
<li><strong>VPS Connection</strong> &mdash; Host, user, port, SSH key path</li>
<li><strong>Hosting Provider API</strong> &mdash; Provider selection, API key, documentation links</li>
<li><strong>Domain &amp; Paths</strong> &mdash; Domain, web root, compose path</li>
</ul>
<h2>17. Front Page</h2>
<p>Manage the public-facing landing page for your domain. Edit content,
configure styling, and deploy updates.</p>
<h2>18. Keyboard Shortcuts &amp; Tips</h2>
<ul style="margin-left:15px;line-height:2">
<li>All actions use AJAX &mdash; the page never fully reloads</li>
<li>Output panels are scrollable; long scan outputs won't overflow</li>
<li>Red text = error, yellow text = warning, green text = success</li>
<li>Every destructive action (uninstall, delete, purge) requires confirmation</li>
<li>SSH connection is shared &mdash; the manager reuses a single SSH session</li>
</ul>
<h2>19. Getting Help</h2>
<ul style="margin-left:15px;line-height:2">
<li>Official repo: <a href="https://repo.seteclabs.io" target="_blank">repo.seteclabs.io</a></li>
<li>GitHub mirror: <a href="https://github.com/DigiJEth" target="_blank">github.com/DigiJEth</a></li>
<li>Submit issues and feature requests at the Gitea repo</li>
</ul>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- HOST API / SSH LINKS -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="doc-hostlinks" class="card" style="display:none">
<div class="card-title">Host API / SSH Links</div>
<div style="font-size:12px;line-height:1.7;color:#ccc">
<p style="color:#888;margin-bottom:15px">Quick-access links to API key generation and SSH key setup guides for every supported hosting provider.</p>
<!-- Hostinger -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Hostinger</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key Gen:</td>
<td>hPanel &rarr; Profile &rarr; API Keys &rarr; Create new key with DNS permissions<br>
<a href="https://developers.hostinger.com" target="_blank">developers.hostinger.com</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>hPanel &rarr; VPS &rarr; Settings &rarr; SSH Keys &rarr; Add SSH Key<br>
<a href="https://support.hostinger.com/en/articles/1583522-how-to-generate-ssh-keys" target="_blank">support.hostinger.com/.../how-to-generate-ssh-keys</a></td>
</tr>
</table>
</div>
<!-- Cloudflare -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Cloudflare</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Token:</td>
<td>dash.cloudflare.com &rarr; My Profile &rarr; API Tokens &rarr; Create Token &rarr; "Edit zone DNS" template<br>
<a href="https://developers.cloudflare.com/api" target="_blank">developers.cloudflare.com/api</a></td>
</tr>
<tr>
<td style="color:#888">SSH (Tunnel):</td>
<td>Cloudflare is a DNS/CDN provider, not a VPS host. If using Cloudflare Tunnel for SSH:<br>
<a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/use-cases/ssh/" target="_blank">developers.cloudflare.com/.../ssh</a></td>
</tr>
</table>
</div>
<!-- DigitalOcean -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>DigitalOcean</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Token:</td>
<td>cloud.digitalocean.com &rarr; API &rarr; Tokens &rarr; Generate New Token (read+write)<br>
<a href="https://docs.digitalocean.com/reference/api" target="_blank">docs.digitalocean.com/reference/api</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Settings &rarr; Security &rarr; SSH Keys &rarr; Add SSH Key<br>
<a href="https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/" target="_blank">docs.digitalocean.com/.../add-ssh-keys</a></td>
</tr>
</table>
</div>
<!-- Vultr -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Vultr</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key:</td>
<td>my.vultr.com &rarr; Account &rarr; API &rarr; Enable API &rarr; copy key (whitelist server IP)<br>
<a href="https://www.vultr.com/api" target="_blank">vultr.com/api</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Account &rarr; SSH Keys &rarr; Add SSH Key<br>
<a href="https://docs.vultr.com/how-do-i-generate-ssh-keys" target="_blank">docs.vultr.com/how-do-i-generate-ssh-keys</a></td>
</tr>
</table>
</div>
<!-- Linode -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Linode (Akamai)</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Token:</td>
<td>cloud.linode.com &rarr; My Profile &rarr; API Tokens &rarr; Create Personal Access Token (Domains read/write)<br>
<a href="https://www.linode.com/docs/api" target="_blank">linode.com/docs/api</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Profile &rarr; SSH Keys &rarr; Add SSH Key (injected into new Linodes at creation)<br>
<a href="https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/" target="_blank">linode.com/docs/.../use-public-key-authentication-with-ssh</a></td>
</tr>
</table>
</div>
<!-- GoDaddy -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>GoDaddy</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key:</td>
<td>developer.godaddy.com &rarr; API Keys &rarr; Create New API Key. Format: <span style="color:#00ff41">key:secret</span><br>
<a href="https://developer.godaddy.com" target="_blank">developer.godaddy.com</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>GoDaddy is primarily a domain registrar. For VPS/dedicated hosting SSH:<br>
<a href="https://www.godaddy.com/help/generate-ssh-keys-40767" target="_blank">godaddy.com/help/generate-ssh-keys-40767</a></td>
</tr>
</table>
</div>
<!-- Namecheap -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Namecheap</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key:</td>
<td>Profile &rarr; Tools &rarr; API Access &rarr; Enable API (requires IP whitelist)<br>
<a href="https://www.namecheap.com/support/api/intro" target="_blank">namecheap.com/support/api/intro</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Namecheap is primarily a domain registrar. For hosting products:<br>
<a href="https://www.namecheap.com/support/knowledgebase/article.aspx/9356/69/how-to-generate-an-ssh-key/" target="_blank">namecheap.com/.../how-to-generate-an-ssh-key</a></td>
</tr>
</table>
</div>
<!-- Hetzner -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Hetzner</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Token:</td>
<td>dns.hetzner.com &rarr; API Tokens &rarr; Create new token<br>
<a href="https://dns.hetzner.com/api-docs" target="_blank">dns.hetzner.com/api-docs</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Cloud Console &rarr; Security &rarr; SSH Keys &rarr; Add SSH Key<br>
<a href="https://docs.hetzner.com/cloud/servers/getting-started/connecting-to-the-server/" target="_blank">docs.hetzner.com/.../connecting-to-the-server</a></td>
</tr>
</table>
</div>
<!-- OVH -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>OVH / OVHcloud</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key:</td>
<td>Requires Application Key, Application Secret, and Consumer Key<br>
<a href="https://api.ovh.com/createApp" target="_blank">api.ovh.com/createApp</a><br>
<a href="https://api.ovh.com" target="_blank">api.ovh.com (documentation)</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>Control Panel &rarr; Public Cloud &rarr; SSH Keys &rarr; Add key<br>
<a href="https://help.ovhcloud.com/csm/en-dedicated-servers-creating-ssh-keys" target="_blank">help.ovhcloud.com/.../creating-ssh-keys</a></td>
</tr>
</table>
</div>
<!-- AWS Route 53 -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>AWS Route 53</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API Key:</td>
<td>IAM Console &rarr; Users &rarr; Create Access Key (needs AmazonRoute53FullAccess). Format: <span style="color:#00ff41">ACCESS_KEY:SECRET_KEY</span><br>
<a href="https://docs.aws.amazon.com/Route53/latest/APIReference" target="_blank">docs.aws.amazon.com/Route53/latest/APIReference</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>EC2 Console &rarr; Key Pairs &rarr; Create/Import key pair (downloads .pem file)<br>
<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html" target="_blank">docs.aws.amazon.com/.../ec2-key-pairs.html</a></td>
</tr>
</table>
</div>
<!-- Contabo -->
<div style="border:1px solid #333;padding:12px;margin-bottom:10px;background:#0a0a0a">
<p style="color:#00ff41;font-size:13px;margin-bottom:8px"><strong>Contabo</strong></p>
<table style="width:100%">
<tr>
<td style="color:#888;width:120px">API:</td>
<td>Contabo does not provide a DNS API. Use a third-party DNS provider (Cloudflare, etc.) or manage records manually.<br>
<a href="https://api.contabo.com" target="_blank">api.contabo.com (server management API only)</a></td>
</tr>
<tr>
<td style="color:#888">SSH Key Guide:</td>
<td>No panel-based SSH key manager. Generate keys locally and copy with <code style="color:#00ff41">ssh-copy-id</code><br>
<a href="https://contabo.com/blog/establishing-connection-server-ssh/" target="_blank">contabo.com/blog/establishing-connection-server-ssh</a></td>
</tr>
</table>
</div>
<!-- Generic SSH keygen -->
<div style="border:1px solid #00ff41;padding:12px;margin-top:15px;background:#0a0a0a">
<p style="color:#88ff88;font-size:13px;margin-bottom:8px"><strong>Universal: Generate SSH Keys (any provider)</strong></p>
<div style="background:#000;padding:10px;border:1px solid #333;margin:8px 0;font-size:11px">
<span style="color:#888"># Generate ed25519 key pair (recommended)</span><br>
<span style="color:#00ff41">ssh-keygen -t ed25519 -f C:/keys/setec -N ""</span><br><br>
<span style="color:#888"># Copy public key to server</span><br>
<span style="color:#00ff41">ssh-copy-id -i C:/keys/setec.pub -p 2222 root@YOUR_SERVER_IP</span><br><br>
<span style="color:#888"># Test connection</span><br>
<span style="color:#00ff41">ssh -i C:/keys/setec -p 2222 root@YOUR_SERVER_IP</span><br><br>
<span style="color:#888"># (Alternative) RSA 4096-bit key</span><br>
<span style="color:#00ff41">ssh-keygen -t rsa -b 4096 -f C:/keys/setec -N ""</span>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- TROUBLESHOOTING GUIDE -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="doc-troubleshoot" class="card" style="display:none">
<div class="card-title">Troubleshooting Guide</div>
<div style="font-size:12px;line-height:1.7;color:#ccc">
<!-- SSH Connection -->
<h2>SSH Connection Issues</h2>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "Connection refused" or "Connection timed out"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Verify the server IP is correct and the VPS is powered on</li>
<li>Check the SSH port matches what's configured: <code style="color:#00ff41">nc -zv YOUR_IP YOUR_PORT</code></li>
<li>Confirm the port is open on the server firewall:
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">ufw status</span> &nbsp;or&nbsp; <span style="color:#00ff41">iptables -L -n | grep YOUR_PORT</span>
</div>
</li>
<li>Check if your home IP is blocked (fail2ban, CSF, or hosting provider firewall)</li>
</ul>
</div>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "Permission denied (publickey)"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Verify the key path in Settings points to your <strong>private</strong> key (not .pub)</li>
<li>Check the public key is in the server's authorized_keys:
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">cat ~/.ssh/authorized_keys</span>
</div>
</li>
<li>Fix permissions on the server:
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys</span>
</div>
</li>
<li>Test manually with verbose output:
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">ssh -i /path/to/key -p PORT user@IP -vvv</span>
</div>
</li>
</ul>
</div>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "Host key verification failed"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>The server's fingerprint changed (reinstall, IP reassignment, or MITM)</li>
<li>Remove the old key: <code style="color:#00ff41">ssh-keygen -R YOUR_IP</code></li>
<li>Reconnect and verify the new fingerprint with your hosting provider</li>
</ul>
</div>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>SSH connects but commands time out</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Server may be under heavy load &mdash; check CPU/RAM in your hosting panel</li>
<li>Long-running commands (full scans, package installs) have extended timeouts but may still exceed them</li>
<li>Try running the command directly from Terminal page for real-time output</li>
</ul>
</div>
<!-- DNS API -->
<h2>DNS API Issues</h2>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "HTTP 401" or "HTTP 403"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>API key is invalid, expired, or has insufficient permissions</li>
<li>Regenerate a new API key from your provider's dashboard (see Host API Links)</li>
<li>Ensure the key has DNS read/write permissions</li>
<li>Some providers (Vultr, Namecheap) require IP whitelisting</li>
</ul>
</div>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Error: "HTTP 530" or Cloudflare blocking API calls</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>If your domain uses Cloudflare as a proxy, API calls from your VPS may be intercepted</li>
<li>SETEC Manager routes API calls through your VPS via SSH to bypass this</li>
<li>If still failing, try making the API call directly from your local machine</li>
<li>Check if the provider's API endpoint is behind Cloudflare</li>
</ul>
</div>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>DNS changes not propagating</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>DNS propagation takes time (minutes to 48 hours depending on TTL)</li>
<li>Check current state: <code style="color:#00ff41">dig +short A yourdomain.com</code></li>
<li>Check with different resolvers: <code style="color:#00ff41">dig @8.8.8.8 +short A yourdomain.com</code></li>
<li>Lower your TTL to 300 before making changes for faster propagation</li>
</ul>
</div>
<!-- Security Tools -->
<h2>Security Tool Issues</h2>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Tool install fails with "dpkg lock" error</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Another package operation is in progress (apt update, unattended-upgrades)</li>
<li>Wait a few minutes and retry, or check:
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">lsof /var/lib/dpkg/lock-frontend</span>
</div>
</li>
<li>If the process is stuck, kill it (last resort):
<div style="background:#000;padding:6px;margin:4px 0;font-size:11px">
<span style="color:#00ff41">kill -9 PID && dpkg --configure -a</span>
</div>
</li>
</ul>
</div>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>ClamAV: "freshclam" or virus DB errors</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Stop freshclam service before manual update: <code style="color:#00ff41">systemctl stop clamav-freshclam</code></li>
<li>Run manual update: <code style="color:#00ff41">freshclam</code></li>
<li>ClamAV CDN may rate-limit &mdash; wait and retry in 30 minutes</li>
<li>Check if DNS resolves: <code style="color:#00ff41">dig database.clamav.net</code></li>
</ul>
</div>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>Scans/audits appear to hang or run forever</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Full system scans (ClamAV, Lynis, AIDE) can take 10-60+ minutes</li>
<li>Use "Quick Scan" options when available for faster results</li>
<li>Check server load &mdash; scans are CPU-intensive</li>
<li>For long scans, use the Terminal page for real-time output</li>
</ul>
</div>
<!-- Firewall -->
<h2>Firewall Issues</h2>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Locked out of server after firewall change</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li style="color:#ff4444"><strong>Prevention:</strong> ALWAYS allow your SSH port before enabling the firewall</li>
<li>Use your hosting provider's console/VNC access to regain control</li>
<li>From console: <code style="color:#00ff41">ufw allow 2222/tcp && ufw reload</code></li>
<li>Or disable the firewall entirely: <code style="color:#00ff41">ufw disable</code></li>
</ul>
</div>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>Multiple firewalls conflicting</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Only run ONE firewall at a time (UFW, iptables raw, nftables, firewalld, or CSF)</li>
<li>UFW is a frontend for iptables &mdash; they share the same backend</li>
<li>Use the Migration tabs (UFW&harr;iptables) to safely switch</li>
<li>Check what's active: <code style="color:#00ff41">ufw status</code>, <code style="color:#00ff41">iptables -L -n</code>, <code style="color:#00ff41">nft list ruleset</code></li>
</ul>
</div>
<!-- Nginx / SSL -->
<h2>Nginx / SSL Issues</h2>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Certbot SSL fails: "DNS problem: NXDOMAIN"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>The domain/subdomain doesn't have a DNS A record pointing to your server</li>
<li>Add the A record first, wait for propagation, then retry certbot</li>
<li>Verify: <code style="color:#00ff41">dig +short A subdomain.yourdomain.com</code></li>
</ul>
</div>
<div style="border-left:3px solid #ff4444;padding:8px 12px;margin:10px 0;background:#1a0a0a">
<p style="color:#ff4444;margin-bottom:5px"><strong>Nginx won't start: "address already in use"</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Another process is using port 80/443: <code style="color:#00ff41">ss -tlnp | grep ':80\|:443'</code></li>
<li>Common culprit: Apache. Stop it: <code style="color:#00ff41">systemctl stop apache2 && systemctl disable apache2</code></li>
</ul>
</div>
<!-- General -->
<h2>General Issues</h2>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>Manager shows "Loading..." forever</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>SSH connection dropped &mdash; refresh the page to reconnect</li>
<li>Check that <code style="color:#00ff41">python app.py</code> is still running in your terminal</li>
<li>Check browser console (F12) for JavaScript errors</li>
</ul>
</div>
<div style="border-left:3px solid #ffaa00;padding:8px 12px;margin:10px 0;background:#1a1a0a">
<p style="color:#ffaa00;margin-bottom:5px"><strong>Settings not saving / resetting on restart</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Config is stored at <code style="color:#00ff41">~/.setec-mgr/config.json</code></li>
<li>Check file permissions: <code style="color:#00ff41">ls -la ~/.setec-mgr/</code></li>
<li>View current config: <code style="color:#00ff41">cat ~/.setec-mgr/config.json</code></li>
</ul>
</div>
<div style="border:1px solid #00ff41;padding:12px;margin-top:20px;background:#0a0a0a">
<p style="color:#88ff88;margin-bottom:5px"><strong>Still need help?</strong></p>
<ul style="margin-left:15px;color:#888;line-height:1.8">
<li>Submit a ticket: <a href="https://repo.seteclabs.io" target="_blank">repo.seteclabs.io</a></li>
<li>GitHub mirror: <a href="https://github.com/DigiJEth" target="_blank">github.com/DigiJEth</a></li>
<li>Include: error message, server OS, SETEC Manager version, and steps to reproduce</li>
</ul>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
function showDoc(id) {
document.getElementById('doc-manual').style.display = (id === 'manual') ? 'block' : 'none';
document.getElementById('doc-hostlinks').style.display = (id === 'hostlinks') ? 'block' : 'none';
document.getElementById('doc-troubleshoot').style.display = (id === 'troubleshoot') ? 'block' : 'none';
}
</script>
{% endblock %}

View File

@@ -0,0 +1,168 @@
{% extends "base.html" %}
{% block title %}Fail2Ban{% endblock %}
{% block content %}
<h1>[!] Fail2Ban</h1>
<div class="toolbar">
<button class="btn" onclick="loadStatus()">Refresh</button>
<button class="btn" onclick="loadAllJails()">All Jails Detail</button>
<button class="btn" onclick="loadLog()">View Log</button>
<button class="btn" onclick="loadConfig()">Edit Config</button>
<button class="btn btn-warn" onclick="reloadF2B()">Reload</button>
<button class="btn btn-danger" onclick="unbanAll()">Unban All</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Status</div>
<div class="output" id="status-output"><span class="info">Loading...</span></div>
</div>
<div class="card">
<div class="card-title">Jail Details</div>
<div class="toolbar">
<select id="jail-select" onchange="loadJail()">
<option value="">-- select jail --</option>
<option value="sshd">sshd</option>
<option value="nginx-http-auth">nginx-http-auth</option>
<option value="nginx-botsearch">nginx-botsearch</option>
<option value="nginx-badbots">nginx-badbots</option>
<option value="postfix">postfix</option>
</select>
</div>
<div class="output" id="jail-output"><span class="info">Select a jail</span></div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Ban IP</div>
<label>Jail</label>
<select id="ban-jail">
<option value="sshd">sshd</option>
<option value="nginx-http-auth">nginx-http-auth</option>
<option value="nginx-botsearch">nginx-botsearch</option>
<option value="nginx-badbots">nginx-badbots</option>
<option value="postfix">postfix</option>
</select>
<label>IP Address</label>
<input type="text" id="ban-ip" placeholder="1.2.3.4" style="width:200px">
<br><br>
<button class="btn btn-danger" onclick="banIP()">Ban</button>
</div>
<div class="card">
<div class="card-title">Unban IP</div>
<label>Jail</label>
<select id="unban-jail">
<option value="sshd">sshd</option>
<option value="nginx-http-auth">nginx-http-auth</option>
<option value="nginx-botsearch">nginx-botsearch</option>
<option value="nginx-badbots">nginx-badbots</option>
<option value="postfix">postfix</option>
</select>
<label>IP Address</label>
<input type="text" id="unban-ip" placeholder="1.2.3.4" style="width:200px">
<br><br>
<button class="btn" onclick="unbanIP()">Unban</button>
</div>
</div>
<div class="card" id="config-card" style="display:none">
<div class="card-title">jail.local</div>
<textarea id="config-editor" rows="20" style="width:100%;tab-size:4" spellcheck="false"></textarea>
<br>
<button class="btn" onclick="saveConfig()">Save & Reload</button>
</div>
<div class="card">
<div class="card-title">Output / Log</div>
<div class="output" id="output" style="max-height:500px"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function loadStatus() {
const res = await apiGet('/api/fail2ban/status');
showResult(res, 'status-output');
}
async function loadJail() {
const name = document.getElementById('jail-select').value;
if (!name) return;
const res = await apiGet('/api/fail2ban/jail/' + name);
showResult(res, 'jail-output');
}
async function loadAllJails() {
const res = await apiGet('/api/fail2ban/jails');
showResult(res);
}
async function banIP() {
const jail = document.getElementById('ban-jail').value;
const ip = document.getElementById('ban-ip').value.trim();
if (!ip) { alert('Enter an IP'); return; }
const res = await apiPost('/api/fail2ban/ban', {jail, ip});
showResult(res);
loadJail();
}
async function unbanIP() {
const jail = document.getElementById('unban-jail').value;
const ip = document.getElementById('unban-ip').value.trim();
if (!ip) { alert('Enter an IP'); return; }
const res = await apiPost('/api/fail2ban/unban', {jail, ip});
showResult(res);
loadJail();
}
async function unbanAll() {
if (!confirm('Unban ALL IPs from ALL jails?')) return;
const res = await apiPost('/api/fail2ban/unban-all');
showResult(res);
loadStatus();
}
async function reloadF2B() {
const res = await apiPost('/api/fail2ban/reload');
showResult(res);
loadStatus();
}
async function loadLog() {
const res = await apiGet('/api/fail2ban/log?lines=50');
showResult(res);
}
async function loadConfig() {
document.getElementById('config-card').style.display = '';
const res = await apiGet('/api/fail2ban/config');
if (res.ok) {
document.getElementById('config-editor').value = res.data.stdout || '';
}
}
async function saveConfig() {
const content = document.getElementById('config-editor').value;
if (!confirm('Save config and reload fail2ban?')) return;
const res = await apiPost('/api/fail2ban/config/save', {content});
showResult(res);
loadStatus();
}
document.getElementById('config-editor').addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
e.preventDefault();
const s = this.selectionStart;
this.value = this.value.substring(0, s) + ' ' + this.value.substring(this.selectionEnd);
this.selectionStart = this.selectionEnd = s + 4;
}
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
saveConfig();
}
});
loadStatus();
</script>
{% endblock %}

View File

@@ -0,0 +1,93 @@
{% extends "base.html" %}
{% block title %}Files{% endblock %}
{% block content %}
<h1>[/] File Manager</h1>
<div class="toolbar">
<input type="text" id="file-path" value="/var/www" style="width:400px">
<button class="btn" onclick="listFiles()">List</button>
<button class="btn" onclick="readFile()">Read File</button>
<button class="btn" onclick="mkdirRemote()">mkdir</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Directory Listing</div>
<div class="output" id="dir-output" style="max-height:600px;cursor:pointer" onclick="handleDirClick(event)">
<span class="info">Enter a path and click List</span>
</div>
</div>
<div>
<div class="card">
<div class="card-title">File Editor</div>
<label>Path</label>
<input type="text" id="edit-path" style="width:100%">
<textarea id="edit-content" rows="20" style="width:100%;resize:vertical"></textarea>
<br>
<button class="btn" onclick="saveFile()">Save File</button>
<button class="btn btn-danger" onclick="deleteFile()">Delete</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
async function listFiles() {
const path = document.getElementById('file-path').value;
const res = await apiGet('/api/files/list?path='+encodeURIComponent(path));
showResult(res, 'dir-output');
}
async function readFile() {
const path = document.getElementById('file-path').value;
const res = await apiGet('/api/files/read?path='+encodeURIComponent(path));
if (res.ok) {
document.getElementById('edit-path').value = path;
document.getElementById('edit-content').value = res.data.stdout || '';
}
showResult(res);
}
async function saveFile() {
const path = document.getElementById('edit-path').value;
const content = document.getElementById('edit-content').value;
if (!path) { alert('Enter file path'); return; }
if (!confirm('Save to '+path+'?')) return;
const res = await apiPost('/api/files/write', {path, content});
showResult(res);
}
async function deleteFile() {
const path = document.getElementById('edit-path').value || document.getElementById('file-path').value;
if (!path) return;
if (!confirm('DELETE '+path+'? This cannot be undone!')) return;
const res = await apiDelete('/api/files/delete', {path});
showResult(res);
listFiles();
}
async function mkdirRemote() {
const path = document.getElementById('file-path').value;
if (!path) return;
const res = await apiPost('/api/files/mkdir', {path});
showResult(res);
}
function handleDirClick(event) {
// Allow clicking on filenames in the listing
const text = window.getSelection().toString().trim();
if (text && !text.includes('\n')) {
const base = document.getElementById('file-path').value.replace(/\/$/,'');
document.getElementById('file-path').value = base + '/' + text;
}
}
listFiles();
</script>
{% endblock %}

View File

@@ -0,0 +1,990 @@
{% extends "base.html" %}
{% block title %}Firewall{% endblock %}
{% block content %}
<h1>[|] Firewall Manager</h1>
<!-- Tab navigation -->
<div class="toolbar" id="tabs">
<button class="btn" onclick="showTab('dashboard')" id="tab-dashboard">Dashboard</button>
<button class="btn" onclick="showTab('ufw')" id="tab-ufw">UFW</button>
<button class="btn" onclick="showTab('iptables')" id="tab-iptables">iptables</button>
<button class="btn" onclick="showTab('nftables')" id="tab-nftables">nftables</button>
<button class="btn" onclick="showTab('firewalld')" id="tab-firewalld">firewalld</button>
<button class="btn" onclick="showTab('csf')" id="tab-csf">CSF</button>
<button class="btn" onclick="showTab('ufw2ip')" id="tab-ufw2ip">UFW → IP</button>
<button class="btn" onclick="showTab('ip2ufw')" id="tab-ip2ufw">IP → UFW</button>
</div>
<!-- ═══════════════════════ DASHBOARD TAB ═══════════════════════ -->
<div class="tab-content" id="panel-dashboard">
<div class="grid grid-2">
<div class="card">
<div class="card-title">Firewall Detection</div>
<div id="fw-detect" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwDetect()">Detect Firewalls</button>
</div>
<div class="card">
<div class="card-title">Open Ports</div>
<div id="fw-ports" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwPorts()">Scan Ports</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Active Connections</div>
<div id="fw-conns" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwConns()">Refresh</button>
</div>
<div class="card">
<div class="card-title">Connection Stats by State</div>
<div id="fw-connstats" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwConnStats()">Refresh</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Top Connections by IP</div>
<div id="fw-topip" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwTopIPs()">Refresh</button>
</div>
<div class="card">
<div class="card-title">Recent Blocked / Dropped</div>
<div id="fw-blocked" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwBlocked()">Refresh</button>
</div>
</div>
<div class="card">
<div class="card-title">Firewall Log</div>
<div id="fw-log" class="output" style="max-height:400px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwLog()">Load Log</button>
</div>
</div>
<!-- ═══════════════════════ UFW TAB ═══════════════════════ -->
<div class="tab-content" id="panel-ufw" style="display:none;">
<div class="card">
<div class="card-title">UFW Status</div>
<div id="ufw-status" class="output" style="max-height:400px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="ufwStatus()">Refresh</button>
<button class="btn btn-warn" onclick="ufwEnable()">Enable UFW</button>
<button class="btn btn-danger" onclick="ufwDisable()">Disable UFW</button>
<button class="btn" onclick="ufwNumbered()">Numbered Rules</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Add Rule</div>
<label>Rule (e.g. "allow 8080/tcp", "deny from 1.2.3.4")</label>
<input type="text" id="ufw-rule" placeholder="allow 443/tcp" style="width:100%;">
<button class="btn" onclick="ufwAdd()" style="margin-top:8px;">Add Rule</button>
</div>
<div class="card">
<div class="card-title">Delete Rule</div>
<label>Rule to delete (e.g. "allow 8080/tcp")</label>
<input type="text" id="ufw-del-rule" placeholder="allow 8080/tcp" style="width:100%;">
<button class="btn btn-danger" onclick="ufwDel()" style="margin-top:8px;">Delete Rule</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Presets</div>
<button class="btn" onclick="ufwPreset('webserver')">Web Server (80, 443)</button>
<button class="btn" onclick="ufwPreset('mailserver')">Mail Server (25, 465, 587, 993)</button>
<button class="btn btn-danger" onclick="ufwPreset('lockdown')">Lockdown (SSH only)</button>
</div>
<div class="card">
<div class="card-title">Defaults</div>
<div id="ufw-defaults" class="output" style="max-height:150px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="ufwDefault('deny','incoming')">Default Deny In</button>
<button class="btn" onclick="ufwDefault('allow','outgoing')">Default Allow Out</button>
<button class="btn btn-danger" onclick="ufwDefault('deny','outgoing')">Default Deny Out</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">UFW Application Profiles</div>
<div id="ufw-apps" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="ufwAppList()">List App Profiles</button>
</div>
<div class="card">
<div class="card-title">UFW Log</div>
<div id="ufw-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="ufwLog()">View Log</button>
<button class="btn" onclick="ufwLogLevel('on')">Logging On</button>
<button class="btn" onclick="ufwLogLevel('high')">Logging High</button>
<button class="btn btn-danger" onclick="ufwLogLevel('off')">Logging Off</button>
</div>
</div>
</div>
<!-- ═══════════════════════ IPTABLES TAB ═══════════════════════ -->
<div class="tab-content" id="panel-iptables" style="display:none;">
<div id="ipt-not-installed" style="display:none;">
<div class="card">
<div class="card-title">iptables</div>
<p style="color:#888;margin-bottom:10px;">iptables is the traditional Linux packet filter. It is usually pre-installed.</p>
<div id="ipt-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<button class="btn btn-warn" onclick="iptInstall()">Install iptables</button>
</div>
</div>
<div id="ipt-main">
<div class="card">
<div class="card-title">iptables Rules (filter)</div>
<div id="ipt-rules" class="output" style="max-height:500px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="iptList()">Filter Table</button>
<button class="btn" onclick="iptListNat()">NAT Table</button>
<button class="btn" onclick="iptListMangle()">Mangle Table</button>
<button class="btn" onclick="iptCounters()">Counters</button>
<button class="btn" onclick="iptIp6()">IPv6 Rules</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Add Rule</div>
<label>Chain</label>
<select id="ipt-chain" style="width:120px;">
<option>INPUT</option>
<option>OUTPUT</option>
<option>FORWARD</option>
</select>
<label>Rule (e.g. "-p tcp --dport 80 -j ACCEPT")</label>
<input type="text" id="ipt-rule" placeholder="-p tcp --dport 80 -j ACCEPT" style="width:100%;">
<div style="margin-top:8px;">
<button class="btn" onclick="iptAdd()">Append Rule</button>
<label style="display:inline;">Position</label>
<input type="number" id="ipt-pos" value="1" style="width:50px;">
<button class="btn" onclick="iptInsert()">Insert at Position</button>
</div>
</div>
<div class="card">
<div class="card-title">Delete Rule</div>
<label>Chain</label>
<select id="ipt-del-chain" style="width:120px;">
<option>INPUT</option>
<option>OUTPUT</option>
<option>FORWARD</option>
</select>
<label>Rule Number</label>
<input type="number" id="ipt-del-num" value="1" style="width:80px;">
<button class="btn btn-danger" onclick="iptDelete()" style="margin-top:8px;">Delete Rule</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Chain Policy</div>
<label>Chain</label>
<select id="ipt-pol-chain" style="width:120px;">
<option>INPUT</option>
<option>OUTPUT</option>
<option>FORWARD</option>
</select>
<label>Policy</label>
<select id="ipt-pol-target" style="width:120px;">
<option>ACCEPT</option>
<option>DROP</option>
</select>
<button class="btn btn-warn" onclick="iptPolicy()" style="margin-top:8px;">Set Policy</button>
</div>
<div class="card">
<div class="card-title">Quick Actions</div>
<div style="margin-bottom:8px;">
<label>IP Address</label>
<input type="text" id="ipt-ip" placeholder="1.2.3.4" style="width:160px;">
</div>
<div class="toolbar">
<button class="btn btn-danger" onclick="iptBlockIP()">Block IP</button>
<button class="btn" onclick="iptUnblockIP()">Unblock IP</button>
<button class="btn" onclick="iptListBlocked()">Show Blocked</button>
</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Persistence</div>
<div id="ipt-persist" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn btn-warn" onclick="iptSave()">Save Rules</button>
<button class="btn" onclick="iptRestore()">Restore Saved</button>
</div>
</div>
<div class="card">
<div class="card-title">Flush</div>
<div id="ipt-flush-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn btn-danger" onclick="iptFlush()">Flush All Rules</button>
<button class="btn" onclick="iptZero()">Zero Counters</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">iptables Log</div>
<div id="ipt-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="iptLog()">View Log</button>
</div>
</div>
</div>
<!-- ═══════════════════════ NFTABLES TAB ═══════════════════════ -->
<div class="tab-content" id="panel-nftables" style="display:none;">
<div id="nft-not-installed" style="display:none;">
<div class="card">
<div class="card-title">nftables</div>
<p style="color:#888;margin-bottom:10px;">nftables is the modern replacement for iptables. Provides better performance and a cleaner syntax.</p>
<div id="nft-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<button class="btn btn-warn" onclick="nftInstall()">Install nftables</button>
</div>
</div>
<div id="nft-main">
<div class="card">
<div class="card-title">nftables Ruleset</div>
<div id="nft-rules" class="output" style="max-height:500px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="nftList()">Full Ruleset</button>
<button class="btn" onclick="nftTables()">Tables</button>
<button class="btn" onclick="nftCounters()">Counters</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">List Chains</div>
<div id="nft-chains" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<label>Table (e.g. "inet filter")</label>
<input type="text" id="nft-table" value="inet filter" style="width:200px;">
<button class="btn" onclick="nftChains()" style="margin-top:8px;">List Chains</button>
</div>
<div class="card">
<div class="card-title">Add Rule</div>
<label>Table</label>
<input type="text" id="nft-add-table" value="inet filter" style="width:180px;">
<label>Chain</label>
<input type="text" id="nft-add-chain" value="input" style="width:180px;">
<label>Rule (e.g. "tcp dport 80 accept")</label>
<input type="text" id="nft-add-rule" placeholder="tcp dport 80 accept" style="width:100%;">
<button class="btn" onclick="nftAddRule()" style="margin-top:8px;">Add Rule</button>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Delete Rule</div>
<label>Table</label>
<input type="text" id="nft-del-table" value="inet filter" style="width:180px;">
<label>Chain</label>
<input type="text" id="nft-del-chain" value="input" style="width:180px;">
<label>Handle Number</label>
<input type="number" id="nft-del-handle" style="width:80px;">
<button class="btn btn-danger" onclick="nftDelRule()" style="margin-top:8px;">Delete Rule</button>
</div>
<div class="card">
<div class="card-title">Create Table / Chain</div>
<label>Family</label>
<select id="nft-family" style="width:100px;">
<option>inet</option>
<option>ip</option>
<option>ip6</option>
<option>bridge</option>
</select>
<label>Table Name</label>
<input type="text" id="nft-new-table" placeholder="mytable" style="width:150px;">
<button class="btn" onclick="nftCreateTable()" style="margin-top:8px;">Create Table</button>
<div style="margin-top:10px;border-top:1px solid #333;padding-top:8px;">
<label>Chain Name</label>
<input type="text" id="nft-new-chain" placeholder="mychain" style="width:150px;">
<label>Hook</label>
<select id="nft-hook" style="width:100px;">
<option>input</option>
<option>output</option>
<option>forward</option>
<option>prerouting</option>
<option>postrouting</option>
</select>
<button class="btn" onclick="nftCreateChain()" style="margin-top:8px;">Create Chain</button>
</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Persistence</div>
<div id="nft-persist" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn btn-warn" onclick="nftSave()">Save Ruleset</button>
<button class="btn" onclick="nftRestore()">Restore Saved</button>
<button class="btn" onclick="nftConfig()">View Config File</button>
</div>
</div>
<div class="card">
<div class="card-title">Flush</div>
<div id="nft-flush-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<button class="btn btn-danger" onclick="nftFlush()">Flush All</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════ FIREWALLD TAB ═══════════════════════ -->
<div class="tab-content" id="panel-firewalld" style="display:none;">
<div id="fwd-not-installed" style="display:none;">
<div class="card">
<div class="card-title">firewalld</div>
<p style="color:#888;margin-bottom:10px;">firewalld is a zone-based firewall manager. Common on RHEL/CentOS but also available on Debian/Ubuntu.</p>
<div id="fwd-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<button class="btn btn-warn" onclick="fwdInstall()">Install firewalld</button>
</div>
</div>
<div id="fwd-main">
<div class="grid grid-2">
<div class="card">
<div class="card-title">firewalld Status</div>
<div id="fwd-status" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="fwdStatus()">Refresh</button>
<button class="btn btn-warn" onclick="fwdReload()">Reload</button>
<button class="btn btn-danger" onclick="fwdPanicOn()">Panic On</button>
<button class="btn" onclick="fwdPanicOff()">Panic Off</button>
</div>
</div>
<div class="card">
<div class="card-title">Zones</div>
<div id="fwd-zones" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwdZones()">List Zones</button>
<div style="margin-top:8px;">
<label>Zone</label>
<input type="text" id="fwd-zone" value="public" style="width:120px;">
<button class="btn" onclick="fwdZoneInfo()">Zone Info</button>
<button class="btn btn-warn" onclick="fwdSetDefault()">Set as Default</button>
</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Services</div>
<div id="fwd-services" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<label>Service (e.g. http, https, ssh, smtp)</label>
<input type="text" id="fwd-service" placeholder="http" style="width:150px;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn" onclick="fwdServicesList()">List Available</button>
<button class="btn btn-warn" onclick="fwdAddService()">Add Service</button>
<button class="btn btn-danger" onclick="fwdRemoveService()">Remove Service</button>
</div>
</div>
<div class="card">
<div class="card-title">Ports</div>
<div id="fwd-ports" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<label>Port (e.g. 8080/tcp)</label>
<input type="text" id="fwd-port" placeholder="8080/tcp" style="width:150px;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn btn-warn" onclick="fwdAddPort()">Add Port</button>
<button class="btn btn-danger" onclick="fwdRemovePort()">Remove Port</button>
</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Rich Rules</div>
<div id="fwd-rich" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<label>Rich Rule</label>
<input type="text" id="fwd-rich-rule" placeholder='rule family="ipv4" source address="1.2.3.4" drop' style="width:100%;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn btn-warn" onclick="fwdAddRich()">Add Rich Rule</button>
<button class="btn btn-danger" onclick="fwdRemoveRich()">Remove Rich Rule</button>
</div>
</div>
<div class="card">
<div class="card-title">Block / Unblock IP</div>
<div id="fwd-block" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<label>IP Address</label>
<input type="text" id="fwd-ip" placeholder="1.2.3.4" style="width:160px;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn btn-danger" onclick="fwdBlockIP()">Block IP</button>
<button class="btn" onclick="fwdUnblockIP()">Unblock IP</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">firewalld Log</div>
<div id="fwd-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="fwdLog()">View Log</button>
</div>
</div>
</div>
<!-- ═══════════════════════ CSF TAB ═══════════════════════ -->
<div class="tab-content" id="panel-csf" style="display:none;">
<div id="csf-not-installed" style="display:none;">
<div class="card">
<div class="card-title">CSF (ConfigServer Security & Firewall)</div>
<p style="color:#888;margin-bottom:10px;">CSF is a stateful packet inspection firewall with login/intrusion detection (LFD). Popular on cPanel servers but works standalone.</p>
<div id="csf-install-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<button class="btn btn-warn" onclick="csfInstall()">Install CSF</button>
</div>
</div>
<div id="csf-main">
<div class="grid grid-2">
<div class="card">
<div class="card-title">CSF Status</div>
<div id="csf-status" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="csfStatus()">Refresh</button>
<button class="btn btn-warn" onclick="csfStart()">Start</button>
<button class="btn btn-danger" onclick="csfStop()">Stop (Flush)</button>
<button class="btn" onclick="csfRestart()">Restart</button>
</div>
</div>
<div class="card">
<div class="card-title">Test iptables Modules</div>
<div id="csf-test" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="csfTest()">Run Test</button>
</div>
</div>
<div class="card">
<div class="card-title">Rules</div>
<div id="csf-rules" class="output" style="max-height:500px;margin-bottom:10px;"></div>
<button class="btn" onclick="csfList()">List All Rules</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Allow / Deny IP</div>
<div id="csf-ip-out" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<label>IP Address</label>
<input type="text" id="csf-ip" placeholder="1.2.3.4" style="width:160px;">
<label>Comment</label>
<input type="text" id="csf-comment" placeholder="reason" style="width:200px;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn" onclick="csfAllow()">Allow</button>
<button class="btn btn-danger" onclick="csfDeny()">Deny</button>
<button class="btn" onclick="csfRemove()">Remove</button>
<button class="btn" onclick="csfGrep()">Search IP</button>
</div>
</div>
<div class="card">
<div class="card-title">Temporary Rules</div>
<div id="csf-temp" class="output" style="max-height:200px;margin-bottom:10px;"></div>
<label>TTL (seconds)</label>
<input type="number" id="csf-ttl" value="3600" style="width:100px;">
<div class="toolbar" style="margin-top:8px;">
<button class="btn" onclick="csfTempAllow()">Temp Allow</button>
<button class="btn btn-danger" onclick="csfTempDeny()">Temp Deny</button>
<button class="btn" onclick="csfTempList()">List Temp Rules</button>
</div>
</div>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Configuration</div>
<div id="csf-config" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="csfConfig()">View Key Settings</button>
</div>
<div class="card">
<div class="card-title">LFD Log</div>
<div id="csf-log" class="output" style="max-height:300px;margin-bottom:10px;"></div>
<button class="btn" onclick="csfLog()">View Log</button>
</div>
</div>
</div>
</div>
<!-- ═══════════════════════ UFW → IPTABLES TAB ═══════════════════════ -->
<div class="tab-content" id="panel-ufw2ip" style="display:none;">
<div class="card">
<div class="card-title">Migrate: UFW → iptables</div>
<p style="color:#888;margin-bottom:15px;">
This will disable UFW and switch to raw iptables management. Your existing firewall rules
(which UFW manages via iptables under the hood) will be preserved and saved to
<code>/etc/iptables/rules.v4</code>. iptables-persistent will be installed to ensure
rules survive reboots.
</p>
<div style="margin-bottom:15px;padding:10px;border:1px solid #ffaa00;color:#ffaa00;">
<strong>What happens:</strong><br>
1. Current UFW/iptables state is backed up<br>
2. UFW is disabled and its service stopped<br>
3. The iptables rules that UFW generated are saved<br>
4. iptables-persistent is installed for persistence<br>
5. You manage rules directly via the iptables tab
</div>
<div id="m-ufw2ip-out" class="output" style="max-height:500px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="previewUfw2Ip()">Preview Current Rules</button>
<button class="btn btn-danger" onclick="runUfw2Ip()">Migrate UFW → iptables</button>
</div>
</div>
</div>
<!-- ═══════════════════════ IPTABLES → UFW TAB ═══════════════════════ -->
<div class="tab-content" id="panel-ip2ufw" style="display:none;">
<div class="card">
<div class="card-title">Migrate: iptables → UFW</div>
<p style="color:#888;margin-bottom:15px;">
This will convert your iptables rules to UFW and enable UFW as the primary firewall frontend.
Your current iptables rules are backed up before any changes. TCP/UDP ACCEPT rules on INPUT
are automatically converted to UFW allow rules.
</p>
<div style="margin-bottom:15px;padding:10px;border:1px solid #ffaa00;color:#ffaa00;">
<strong>What happens:</strong><br>
1. Current iptables rules are backed up<br>
2. UFW is installed (if not present) and reset<br>
3. Default deny incoming / allow outgoing is set<br>
4. iptables ACCEPT rules are converted to UFW allow rules<br>
5. UFW is enabled, iptables-persistent is disabled<br>
6. You manage rules via the UFW tab
</div>
<div id="m-ip2ufw-out" class="output" style="max-height:500px;margin-bottom:10px;"></div>
<div class="toolbar">
<button class="btn" onclick="previewIp2Ufw()">Preview Current Rules</button>
<button class="btn btn-danger" onclick="runIp2Ufw()">Migrate iptables → UFW</button>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// ── Tab switching ──
function showTab(name) {
document.querySelectorAll('.tab-content').forEach(el => el.style.display = 'none');
document.querySelectorAll('#tabs .btn').forEach(el => el.style.background = '');
document.getElementById('panel-' + name).style.display = 'block';
document.getElementById('tab-' + name).style.background = '#1a2a1a';
}
showTab('dashboard');
// ── Dashboard ──
async function fwDetect() {
const r = await apiGet('/api/firewall/detect');
showResult(r, 'fw-detect');
}
async function fwPorts() {
const r = await apiGet('/api/firewall/ports');
showResult(r, 'fw-ports');
}
async function fwConns() {
const r = await apiGet('/api/firewall/connections');
showResult(r, 'fw-conns');
}
async function fwConnStats() {
const r = await apiGet('/api/firewall/connection-stats');
showResult(r, 'fw-connstats');
}
async function fwTopIPs() {
const r = await apiGet('/api/firewall/top-ips');
showResult(r, 'fw-topip');
}
async function fwBlocked() {
const r = await apiGet('/api/firewall/blocked');
showResult(r, 'fw-blocked');
}
async function fwLog() {
const r = await apiGet('/api/firewall/log');
showResult(r, 'fw-log');
}
// ── UFW ──
async function ufwStatus() {
const r = await apiGet('/api/security/firewall/status');
showResult(r, 'ufw-status');
}
async function ufwNumbered() {
const r = await apiGet('/api/firewall/ufw/numbered');
showResult(r, 'ufw-status');
}
async function ufwEnable() {
const r = await apiPost('/api/security/firewall/enable');
showResult(r, 'ufw-status');
}
async function ufwDisable() {
if (!confirm('Disable UFW?')) return;
const r = await apiPost('/api/security/firewall/disable');
showResult(r, 'ufw-status');
}
async function ufwAdd() {
const rule = document.getElementById('ufw-rule').value;
const r = await apiPost('/api/security/firewall/add', {rule});
showResult(r, 'ufw-status');
}
async function ufwDel() {
const rule = document.getElementById('ufw-del-rule').value;
const r = await apiPost('/api/security/firewall/delete', {rule});
showResult(r, 'ufw-status');
}
async function ufwPreset(name) {
if (!confirm('Apply ' + name + ' firewall preset?')) return;
const r = await apiPost('/api/security/firewall/preset', {preset: name});
showResult(r, 'ufw-status');
}
async function ufwDefault(policy, direction) {
const r = await apiPost('/api/firewall/ufw/default', {policy, direction});
showResult(r, 'ufw-defaults');
}
async function ufwAppList() {
const r = await apiGet('/api/firewall/ufw/app-list');
showResult(r, 'ufw-apps');
}
async function ufwLog() {
const r = await apiGet('/api/firewall/ufw/log');
showResult(r, 'ufw-log');
}
async function ufwLogLevel(level) {
const r = await apiPost('/api/firewall/ufw/log-level', {level});
showResult(r, 'ufw-log');
}
// ── iptables ──
async function iptList() {
const r = await apiGet('/api/firewall/iptables/list');
showResult(r, 'ipt-rules');
}
async function iptListNat() {
const r = await apiGet('/api/firewall/iptables/list-nat');
showResult(r, 'ipt-rules');
}
async function iptListMangle() {
const r = await apiGet('/api/firewall/iptables/list-mangle');
showResult(r, 'ipt-rules');
}
async function iptCounters() {
const r = await apiGet('/api/firewall/iptables/counters');
showResult(r, 'ipt-rules');
}
async function iptIp6() {
const r = await apiGet('/api/firewall/iptables/ip6');
showResult(r, 'ipt-rules');
}
async function iptAdd() {
const chain = document.getElementById('ipt-chain').value;
const rule = document.getElementById('ipt-rule').value;
const r = await apiPost('/api/firewall/iptables/add', {chain, rule});
showResult(r, 'ipt-rules');
}
async function iptInsert() {
const chain = document.getElementById('ipt-chain').value;
const rule = document.getElementById('ipt-rule').value;
const pos = parseInt(document.getElementById('ipt-pos').value);
const r = await apiPost('/api/firewall/iptables/insert', {chain, rule, position: pos});
showResult(r, 'ipt-rules');
}
async function iptDelete() {
const chain = document.getElementById('ipt-del-chain').value;
const rule_num = parseInt(document.getElementById('ipt-del-num').value);
if (!confirm('Delete rule ' + rule_num + ' from ' + chain + '?')) return;
const r = await apiPost('/api/firewall/iptables/delete', {chain, rule_num});
showResult(r, 'ipt-rules');
}
async function iptPolicy() {
const chain = document.getElementById('ipt-pol-chain').value;
const target = document.getElementById('ipt-pol-target').value;
if (!confirm('Set ' + chain + ' policy to ' + target + '?')) return;
const r = await apiPost('/api/firewall/iptables/policy', {chain, target});
showResult(r, 'ipt-rules');
}
async function iptBlockIP() {
const ip = document.getElementById('ipt-ip').value;
if (!ip) return;
const r = await apiPost('/api/firewall/iptables/block-ip', {ip});
showResult(r, 'ipt-rules');
}
async function iptUnblockIP() {
const ip = document.getElementById('ipt-ip').value;
if (!ip) return;
const r = await apiPost('/api/firewall/iptables/unblock-ip', {ip});
showResult(r, 'ipt-rules');
}
async function iptListBlocked() {
const r = await apiGet('/api/firewall/iptables/blocked');
showResult(r, 'ipt-rules');
}
async function iptSave() {
const r = await apiPost('/api/firewall/iptables/save');
showResult(r, 'ipt-persist');
}
async function iptRestore() {
const r = await apiPost('/api/firewall/iptables/restore');
showResult(r, 'ipt-persist');
}
async function iptFlush() {
if (!confirm('Flush ALL iptables rules? This may lock you out if policy is DROP!')) return;
const r = await apiPost('/api/firewall/iptables/flush');
showResult(r, 'ipt-flush-out');
}
async function iptZero() {
const r = await apiPost('/api/firewall/iptables/zero');
showResult(r, 'ipt-flush-out');
}
async function iptLog() {
const r = await apiGet('/api/firewall/iptables/log');
showResult(r, 'ipt-log');
}
async function iptInstall() {
const r = await apiPost('/api/firewall/iptables/install');
showResult(r, 'ipt-install-out');
}
// ── nftables ──
async function nftList() {
const r = await apiGet('/api/firewall/nftables/list');
showResult(r, 'nft-rules');
}
async function nftTables() {
const r = await apiGet('/api/firewall/nftables/tables');
showResult(r, 'nft-rules');
}
async function nftCounters() {
const r = await apiGet('/api/firewall/nftables/counters');
showResult(r, 'nft-rules');
}
async function nftChains() {
const table = document.getElementById('nft-table').value;
const r = await apiPost('/api/firewall/nftables/chains', {table});
showResult(r, 'nft-chains');
}
async function nftAddRule() {
const table = document.getElementById('nft-add-table').value;
const chain = document.getElementById('nft-add-chain').value;
const rule = document.getElementById('nft-add-rule').value;
const r = await apiPost('/api/firewall/nftables/add-rule', {table, chain, rule});
showResult(r, 'nft-rules');
}
async function nftDelRule() {
const table = document.getElementById('nft-del-table').value;
const chain = document.getElementById('nft-del-chain').value;
const handle = parseInt(document.getElementById('nft-del-handle').value);
if (!confirm('Delete rule handle ' + handle + '?')) return;
const r = await apiPost('/api/firewall/nftables/delete-rule', {table, chain, handle});
showResult(r, 'nft-rules');
}
async function nftCreateTable() {
const family = document.getElementById('nft-family').value;
const name = document.getElementById('nft-new-table').value;
const r = await apiPost('/api/firewall/nftables/create-table', {family, name});
showResult(r, 'nft-rules');
}
async function nftCreateChain() {
const table = document.getElementById('nft-family').value + ' ' + document.getElementById('nft-new-table').value;
const chain = document.getElementById('nft-new-chain').value;
const hook = document.getElementById('nft-hook').value;
const r = await apiPost('/api/firewall/nftables/create-chain', {table, chain, hook});
showResult(r, 'nft-rules');
}
async function nftSave() {
const r = await apiPost('/api/firewall/nftables/save');
showResult(r, 'nft-persist');
}
async function nftRestore() {
const r = await apiPost('/api/firewall/nftables/restore');
showResult(r, 'nft-persist');
}
async function nftConfig() {
const r = await apiGet('/api/firewall/nftables/config');
showResult(r, 'nft-persist');
}
async function nftFlush() {
if (!confirm('Flush ALL nftables rules?')) return;
const r = await apiPost('/api/firewall/nftables/flush');
showResult(r, 'nft-flush-out');
}
async function nftInstall() {
document.getElementById('nft-install-out').innerHTML = '<span class="info">Installing nftables...</span>';
const r = await apiPost('/api/firewall/nftables/install');
showResult(r, 'nft-install-out');
}
// ── firewalld ──
async function fwdStatus() {
const r = await apiGet('/api/firewall/firewalld/status');
showResult(r, 'fwd-status');
}
async function fwdReload() {
const r = await apiPost('/api/firewall/firewalld/reload');
showResult(r, 'fwd-status');
}
async function fwdPanicOn() {
if (!confirm('Enable panic mode? ALL network traffic will be blocked!')) return;
const r = await apiPost('/api/firewall/firewalld/panic-on');
showResult(r, 'fwd-status');
}
async function fwdPanicOff() {
const r = await apiPost('/api/firewall/firewalld/panic-off');
showResult(r, 'fwd-status');
}
async function fwdZones() {
const r = await apiGet('/api/firewall/firewalld/zones');
showResult(r, 'fwd-zones');
}
async function fwdZoneInfo() {
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/zone-info', {zone});
showResult(r, 'fwd-zones');
}
async function fwdSetDefault() {
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/default-zone', {zone});
showResult(r, 'fwd-zones');
}
async function fwdServicesList() {
const r = await apiGet('/api/firewall/firewalld/services-list');
showResult(r, 'fwd-services');
}
async function fwdAddService() {
const svc = document.getElementById('fwd-service').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/add-service', {service: svc, zone});
showResult(r, 'fwd-services');
}
async function fwdRemoveService() {
const svc = document.getElementById('fwd-service').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/remove-service', {service: svc, zone});
showResult(r, 'fwd-services');
}
async function fwdAddPort() {
const port = document.getElementById('fwd-port').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/add-port', {port, zone});
showResult(r, 'fwd-ports');
}
async function fwdRemovePort() {
const port = document.getElementById('fwd-port').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/remove-port', {port, zone});
showResult(r, 'fwd-ports');
}
async function fwdAddRich() {
const rule = document.getElementById('fwd-rich-rule').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/add-rich-rule', {rule, zone});
showResult(r, 'fwd-rich');
}
async function fwdRemoveRich() {
const rule = document.getElementById('fwd-rich-rule').value;
const zone = document.getElementById('fwd-zone').value;
const r = await apiPost('/api/firewall/firewalld/remove-rich-rule', {rule, zone});
showResult(r, 'fwd-rich');
}
async function fwdBlockIP() {
const ip = document.getElementById('fwd-ip').value;
const r = await apiPost('/api/firewall/firewalld/block-ip', {ip});
showResult(r, 'fwd-block');
}
async function fwdUnblockIP() {
const ip = document.getElementById('fwd-ip').value;
const r = await apiPost('/api/firewall/firewalld/unblock-ip', {ip});
showResult(r, 'fwd-block');
}
async function fwdLog() {
const r = await apiGet('/api/firewall/firewalld/log');
showResult(r, 'fwd-log');
}
async function fwdInstall() {
document.getElementById('fwd-install-out').innerHTML = '<span class="info">Installing firewalld...</span>';
const r = await apiPost('/api/firewall/firewalld/install');
showResult(r, 'fwd-install-out');
}
// ── CSF ──
async function csfStatus() {
const r = await apiGet('/api/firewall/csf/status');
showResult(r, 'csf-status');
}
async function csfStart() {
const r = await apiPost('/api/firewall/csf/start');
showResult(r, 'csf-status');
}
async function csfStop() {
if (!confirm('Stop CSF? This will flush all rules.')) return;
const r = await apiPost('/api/firewall/csf/stop');
showResult(r, 'csf-status');
}
async function csfRestart() {
const r = await apiPost('/api/firewall/csf/restart');
showResult(r, 'csf-status');
}
async function csfTest() {
const r = await apiGet('/api/firewall/csf/test');
showResult(r, 'csf-test');
}
async function csfList() {
const r = await apiGet('/api/firewall/csf/list');
showResult(r, 'csf-rules');
}
async function csfAllow() {
const ip = document.getElementById('csf-ip').value;
const comment = document.getElementById('csf-comment').value;
const r = await apiPost('/api/firewall/csf/allow', {ip, comment});
showResult(r, 'csf-ip-out');
}
async function csfDeny() {
const ip = document.getElementById('csf-ip').value;
const comment = document.getElementById('csf-comment').value;
const r = await apiPost('/api/firewall/csf/deny', {ip, comment});
showResult(r, 'csf-ip-out');
}
async function csfRemove() {
const ip = document.getElementById('csf-ip').value;
const r = await apiPost('/api/firewall/csf/remove', {ip});
showResult(r, 'csf-ip-out');
}
async function csfGrep() {
const ip = document.getElementById('csf-ip').value;
const r = await apiPost('/api/firewall/csf/grep', {ip});
showResult(r, 'csf-ip-out');
}
async function csfTempAllow() {
const ip = document.getElementById('csf-ip').value;
const ttl = parseInt(document.getElementById('csf-ttl').value);
const r = await apiPost('/api/firewall/csf/temp-allow', {ip, ttl});
showResult(r, 'csf-temp');
}
async function csfTempDeny() {
const ip = document.getElementById('csf-ip').value;
const ttl = parseInt(document.getElementById('csf-ttl').value);
const r = await apiPost('/api/firewall/csf/temp-deny', {ip, ttl});
showResult(r, 'csf-temp');
}
async function csfTempList() {
const r = await apiGet('/api/firewall/csf/temp-list');
showResult(r, 'csf-temp');
}
async function csfConfig() {
const r = await apiGet('/api/firewall/csf/config');
showResult(r, 'csf-config');
}
async function csfLog() {
const r = await apiGet('/api/firewall/csf/log');
showResult(r, 'csf-log');
}
async function csfInstall() {
document.getElementById('csf-install-out').innerHTML = '<span class="info">Installing CSF...</span>';
const r = await apiPost('/api/firewall/csf/install');
showResult(r, 'csf-install-out');
}
// ── Migration: UFW → iptables ──
async function previewUfw2Ip() {
const r = await apiGet('/api/security/firewall/status');
showResult(r, 'm-ufw2ip-out');
}
async function runUfw2Ip() {
if (!confirm('Migrate from UFW to raw iptables? UFW will be disabled. Make sure you have console access as backup!')) return;
document.getElementById('m-ufw2ip-out').innerHTML = '<span class="info">Migrating UFW → iptables...</span>';
const r = await apiPost('/api/firewall/migrate/ufw-to-iptables');
showResult(r, 'm-ufw2ip-out');
}
// ── Migration: iptables → UFW ──
async function previewIp2Ufw() {
const r = await apiGet('/api/firewall/iptables/list');
showResult(r, 'm-ip2ufw-out');
}
async function runIp2Ufw() {
if (!confirm('Migrate from iptables to UFW? Current rules will be converted. Make sure you have console access as backup!')) return;
document.getElementById('m-ip2ufw-out').innerHTML = '<span class="info">Migrating iptables → UFW...</span>';
const r = await apiPost('/api/firewall/migrate/iptables-to-ufw');
showResult(r, 'm-ip2ufw-out');
}
// Auto-load dashboard on page load
fwDetect();
</script>
{% endblock %}

View File

@@ -0,0 +1,182 @@
{% extends "base.html" %}
{% block title %}Front Page Editor{% endblock %}
{% block content %}
<h1>[&lt;] Front Page Editor</h1>
<div class="toolbar">
<button class="btn" onclick="loadFiles()">Refresh Files</button>
<button class="btn" onclick="newFile()">+ New File</button>
<button class="btn" onclick="openPreview()">Preview Site</button>
</div>
<div style="display: flex; gap: 15px; height: calc(100vh - 140px);">
<!-- File list panel -->
<div style="width: 220px; flex-shrink: 0;">
<div class="card" style="height: 100%; overflow-y: auto;">
<div class="card-title">Site Files</div>
<div id="file-list" style="font-size: 12px;"></div>
</div>
</div>
<!-- Editor panel -->
<div style="flex: 1; display: flex; flex-direction: column;">
<div style="margin-bottom: 8px; display: flex; align-items: center; gap: 8px;">
<span id="current-file" style="color: #888; font-size: 12px;">No file selected</span>
<span id="modified-indicator" style="color: #ffaa00; font-size: 12px; display: none;">* modified</span>
<div style="margin-left: auto;">
<button class="btn" onclick="saveFile()" id="save-btn" disabled>Save</button>
<button class="btn btn-danger" onclick="deleteFile()" id="delete-btn" disabled>Delete</button>
</div>
</div>
<textarea id="editor" style="flex: 1; width: 100%; resize: none; font-size: 13px; line-height: 1.5; tab-size: 2;" disabled></textarea>
<div id="output" class="output" style="max-height: 80px; margin-top: 8px;"></div>
</div>
</div>
<script>
let currentFile = null;
let originalContent = '';
const editor = document.getElementById('editor');
const fileList = document.getElementById('file-list');
const currentFileEl = document.getElementById('current-file');
const modifiedEl = document.getElementById('modified-indicator');
const saveBtn = document.getElementById('save-btn');
const deleteBtn = document.getElementById('delete-btn');
editor.addEventListener('input', () => {
const modified = editor.value !== originalContent;
modifiedEl.style.display = modified ? 'inline' : 'none';
});
// Handle Ctrl+S to save
editor.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
if (currentFile) saveFile();
}
// Tab inserts spaces
if (e.key === 'Tab') {
e.preventDefault();
const start = editor.selectionStart;
const end = editor.selectionEnd;
editor.value = editor.value.substring(0, start) + ' ' + editor.value.substring(end);
editor.selectionStart = editor.selectionEnd = start + 2;
}
});
async function loadFiles() {
const res = await apiGet('/api/frontpage/list');
if (!res.ok) {
fileList.innerHTML = '<span class="err">Failed to load</span>';
return;
}
const lines = (res.data.stdout || '').trim().split('\n').filter(l => l.trim());
if (lines.length === 0) {
fileList.innerHTML = '<span style="color:#888">No files found</span>';
return;
}
let html = '';
lines.forEach(line => {
const parts = line.split('|');
const name = parts[0];
const size = parts[1] ? formatSize(parseInt(parts[1])) : '';
const ext = name.split('.').pop();
const icon = ext === 'html' ? '&lt;/&gt;' : ext === 'css' ? '#' : ext === 'js' ? 'js' : '?';
const active = name === currentFile ? 'background:#1a2a1a;color:#fff;' : '';
html += `<div onclick="openFile('${name}')" style="padding:6px 8px;cursor:pointer;border-bottom:1px solid #1a1a1a;${active}" onmouseover="this.style.background='#1a2a1a'" onmouseout="this.style.background='${name === currentFile ? '#1a2a1a' : ''}'">
<span style="color:#555">[${icon}]</span> ${name}
<span style="color:#555;font-size:10px;float:right">${size}</span>
</div>`;
});
fileList.innerHTML = html;
}
async function openFile(name) {
if (currentFile && editor.value !== originalContent) {
if (!confirm('Unsaved changes. Discard?')) return;
}
const res = await apiGet('/api/frontpage/read?file=' + encodeURIComponent(name));
if (!res.ok) {
showResult(res);
return;
}
currentFile = name;
originalContent = res.data.stdout || '';
editor.value = originalContent;
editor.disabled = false;
saveBtn.disabled = false;
deleteBtn.disabled = false;
currentFileEl.textContent = name;
modifiedEl.style.display = 'none';
document.getElementById('output').innerHTML = '<span class="info">Loaded ' + name + '</span>';
loadFiles(); // refresh to highlight active
}
async function saveFile() {
if (!currentFile) return;
const res = await apiPost('/api/frontpage/write', {
file: currentFile,
content: editor.value
});
if (res.ok) {
originalContent = editor.value;
modifiedEl.style.display = 'none';
document.getElementById('output').innerHTML = '<span class="status-ok">Saved ' + currentFile + '</span>';
} else {
showResult(res);
}
}
async function deleteFile() {
if (!currentFile) return;
if (!confirm('Delete ' + currentFile + '?')) return;
const res = await api('/api/frontpage/delete', {
method: 'DELETE',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({file: currentFile})
});
if (res.ok) {
document.getElementById('output').innerHTML = '<span class="status-ok">Deleted ' + currentFile + '</span>';
currentFile = null;
editor.value = '';
editor.disabled = true;
saveBtn.disabled = true;
deleteBtn.disabled = true;
currentFileEl.textContent = 'No file selected';
loadFiles();
} else {
showResult(res);
}
}
async function newFile() {
const name = prompt('File name (e.g. newpage.html):');
if (!name) return;
const res = await apiPost('/api/frontpage/new', {file: name});
if (res.ok) {
await loadFiles();
openFile(name);
} else {
showResult(res);
}
}
async function openPreview() {
const res = await apiGet('/api/frontpage/preview-url');
if (res.ok) {
window.open(res.data, '_blank');
}
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + 'B';
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + 'K';
return (bytes / 1048576).toFixed(1) + 'M';
}
// Load on page open
loadFiles();
</script>
{% endblock %}

View File

@@ -0,0 +1,107 @@
{% extends "base.html" %}
{% block title %}Nginx{% endblock %}
{% block content %}
<h1>[>] Nginx / Subdomains</h1>
<div class="toolbar">
<button class="btn" onclick="loadSites()">Refresh Sites</button>
<button class="btn" onclick="reloadNginx()">Reload Nginx</button>
</div>
<div class="grid grid-2">
<div class="card">
<div class="card-title">Active Sites</div>
<div class="output" id="sites-output"><span class="info">Loading...</span></div>
</div>
<div>
<div class="card">
<div class="card-title">Add Subdomain</div>
<label>Subdomain (e.g. "api" for api.seteclabs.io)</label>
<input type="text" id="sub-name" placeholder="subdomain" style="width:100%">
<label>Type</label>
<select id="sub-type" onchange="toggleSubFields()">
<option value="proxy">Reverse Proxy</option>
<option value="static">Static Files</option>
</select>
<div id="proxy-fields">
<label>Proxy Port</label>
<input type="number" id="sub-port" placeholder="e.g. 8080" style="width:100%">
</div>
<div id="static-fields" style="display:none">
<label>Document Root</label>
<input type="text" id="sub-root" placeholder="/var/www/sub.seteclabs.io" style="width:100%">
</div>
<br>
<button class="btn" onclick="addSubdomain()">Create Vhost</button>
</div>
<div class="card">
<div class="card-title">SSL Certificate</div>
<label>Domain</label>
<input type="text" id="ssl-domain" placeholder="sub.seteclabs.io" style="width:100%">
<br><br>
<button class="btn" onclick="addSSL()">Install SSL (Certbot)</button>
</div>
<div class="card">
<div class="card-title">View Config</div>
<label>Site name</label>
<input type="text" id="config-site" placeholder="site filename" style="width:100%">
<br><br>
<button class="btn" onclick="viewConfig()">View</button>
</div>
</div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
function toggleSubFields() {
const t = document.getElementById('sub-type').value;
document.getElementById('proxy-fields').style.display = t === 'proxy' ? '' : 'none';
document.getElementById('static-fields').style.display = t === 'static' ? '' : 'none';
}
async function loadSites() {
const res = await apiGet('/api/nginx/sites');
showResult(res, 'sites-output');
}
async function reloadNginx() {
const res = await apiPost('/api/nginx/reload');
showResult(res);
}
async function addSubdomain() {
const sub = document.getElementById('sub-name').value;
const type = document.getElementById('sub-type').value;
if (!sub) { alert('Enter subdomain'); return; }
const body = {subdomain: sub};
if (type === 'proxy') body.proxy_port = document.getElementById('sub-port').value;
else body.static_root = document.getElementById('sub-root').value;
const res = await apiPost('/api/nginx/add-subdomain', body);
showResult(res);
loadSites();
}
async function addSSL() {
const domain = document.getElementById('ssl-domain').value;
if (!domain) return;
document.getElementById('output').innerHTML = '<span class="info">Installing SSL for '+domain+'... (may take a minute)</span>';
const res = await apiPost('/api/nginx/ssl/'+domain);
showResult(res);
}
async function viewConfig() {
const site = document.getElementById('config-site').value;
if (!site) return;
const res = await apiGet('/api/nginx/config/'+site);
showResult(res);
}
loadSites();
</script>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,216 @@
{% extends "base.html" %}
{% block title %}Settings{% endblock %}
{% block content %}
<h1>[%] Settings</h1>
<div class="grid grid-2">
<div class="card">
<div class="card-title">VPS Connection</div>
<label>Host</label>
<input type="text" id="s-host" style="width:100%">
<label>User</label>
<input type="text" id="s-user" style="width:100%">
<label>Port</label>
<input type="number" id="s-port" style="width:100%">
<label>SSH Key Path</label>
<input type="text" id="s-key" style="width:100%">
</div>
<div class="card">
<div class="card-title">Domain & Paths</div>
<label>Domain</label>
<input type="text" id="s-domain" style="width:100%">
<label>Web Root</label>
<input type="text" id="s-webroot" style="width:100%">
<label>Compose Path</label>
<input type="text" id="s-compose" style="width:100%">
</div>
</div>
<div class="card">
<div class="card-title">Hosting Provider API</div>
<p style="color:#888;font-size:11px;margin-bottom:10px">Select your hosting provider and enter API credentials for DNS management.</p>
<label>Provider</label>
<select id="s-provider" style="width:100%" onchange="providerChanged()">
<option value="">-- Select Provider --</option>
</select>
<div id="provider-notes" style="font-size:11px;color:#555;margin:5px 3px"></div>
<label id="lbl-apikey">API Key</label>
<input type="text" id="s-apikey" style="width:100%" placeholder="Enter API key">
<div id="provider-docs" style="font-size:11px;margin:5px 3px"></div>
</div>
<div class="card">
<div class="card-title">E2E SSH Encryption</div>
<p style="color:#888;font-size:11px;margin-bottom:10px">
Encrypts all SSH commands with AES-256-GCM before transport. Requires the setec-agent on the VPS.
</p>
<div id="e2e-status" style="margin-bottom:10px;font-size:12px;color:#555">Loading...</div>
<div class="toolbar" style="margin-bottom:0">
<button class="btn" id="btn-e2e-toggle" onclick="toggleE2E()">Enable E2E</button>
<button class="btn" onclick="deployE2E()">Deploy Agent</button>
<button class="btn" onclick="testE2E()">Test Tunnel</button>
</div>
<div class="output" id="e2e-output" style="margin-top:10px;display:none"></div>
</div>
<div class="toolbar">
<button class="btn" onclick="saveSettings()">Save Settings</button>
<button class="btn" onclick="loadSettings()">Reload</button>
<button class="btn" onclick="testConnection()">Test SSH Connection</button>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
let providers = [];
async function loadProviders() {
const res = await apiGet('/api/hosting/providers');
if (!res.ok) return;
providers = res.data;
const sel = document.getElementById('s-provider');
providers.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
sel.appendChild(opt);
});
}
function providerChanged() {
const id = document.getElementById('s-provider').value;
const p = providers.find(x => x.id === id);
if (p) {
document.getElementById('lbl-apikey').textContent = p.api_key_label || 'API Key';
document.getElementById('provider-notes').textContent = p.notes || '';
document.getElementById('provider-docs').innerHTML = p.docs ? '<a href="' + p.docs + '" target="_blank">' + p.docs + '</a>' : '';
} else {
document.getElementById('lbl-apikey').textContent = 'API Key';
document.getElementById('provider-notes').textContent = '';
document.getElementById('provider-docs').innerHTML = '';
}
}
async function loadSettings() {
const res = await apiGet('/api/settings');
if (!res.ok) { showResult(res); return; }
const d = res.data;
document.getElementById('s-host').value = d.vps_host || '';
document.getElementById('s-user').value = d.vps_user || '';
document.getElementById('s-port').value = d.vps_port || 22;
document.getElementById('s-key').value = d.ssh_key_path || '';
document.getElementById('s-domain').value = d.domain || '';
document.getElementById('s-apikey').value = d.hostinger_api_key || '';
document.getElementById('s-webroot').value = d.web_root || '';
document.getElementById('s-compose').value = d.compose_path || '';
if (d.hosting_provider) {
document.getElementById('s-provider').value = d.hosting_provider;
providerChanged();
}
document.getElementById('output').innerHTML = '<span class="info">Settings loaded.</span>';
}
async function saveSettings() {
const body = {
vps_host: document.getElementById('s-host').value,
vps_user: document.getElementById('s-user').value,
vps_port: parseInt(document.getElementById('s-port').value),
ssh_key_path: document.getElementById('s-key').value,
domain: document.getElementById('s-domain').value,
hostinger_api_key: document.getElementById('s-apikey').value,
hosting_provider: document.getElementById('s-provider').value,
web_root: document.getElementById('s-webroot').value,
compose_path: document.getElementById('s-compose').value,
};
const res = await apiPost('/api/settings', body);
showResult(res);
}
async function testConnection() {
document.getElementById('output').innerHTML = '<span class="info">Testing SSH...</span>';
const res = await apiGet('/api/ssh/test');
showResult(res);
}
// ── E2E Tunnel ──
async function loadE2EStatus() {
const res = await apiGet('/api/e2e/status');
const el = document.getElementById('e2e-status');
const btn = document.getElementById('btn-e2e-toggle');
if (!res.ok) { el.innerHTML = '<span class="error">Failed to load E2E status</span>'; return; }
const d = res.data;
const active = d.e2e_enabled && d.e2e_deployed;
let html = 'Status: ';
if (active) {
html += '<span class="status-ok">ACTIVE</span>';
btn.textContent = 'Disable E2E';
} else if (d.e2e_deployed) {
html += '<span class="status-warn">Deployed but disabled</span>';
btn.textContent = 'Enable E2E';
} else {
html += '<span class="status-err">Not deployed</span>';
btn.textContent = 'Enable E2E';
}
html += ' &nbsp;|&nbsp; Agent: <code>' + d.agent_path + '</code>';
el.innerHTML = html;
}
async function toggleE2E() {
const res = await apiGet('/api/e2e/status');
if (!res.ok) return;
const nowEnabled = res.data.e2e_enabled && res.data.e2e_deployed;
const out = document.getElementById('e2e-output');
out.style.display = 'block';
out.innerHTML = '<span class="info">' + (nowEnabled ? 'Disabling' : 'Enabling') + ' E2E...</span>';
const r = await apiPost('/api/e2e/toggle', {enabled: !nowEnabled});
if (r.ok) {
out.innerHTML = '<span class="status-ok">E2E ' + (!nowEnabled ? 'enabled' : 'disabled') + '</span>';
} else {
out.innerHTML = '<span class="error">' + (r.error || 'Failed') + '</span>';
}
loadE2EStatus();
}
async function deployE2E() {
const out = document.getElementById('e2e-output');
out.style.display = 'block';
out.innerHTML = '<span class="info">Deploying agent + tunnel key to VPS... this may take a moment.</span>';
const r = await apiPost('/api/e2e/deploy', {});
if (r.ok) {
let html = '<span class="status-ok">Deployment complete!</span><br>';
(r.data || []).forEach(s => {
html += (s.ok ? '<span class="status-ok">[OK]</span>' : '<span class="status-err">[FAIL]</span>') + ' ' + s.step + '<br>';
});
out.innerHTML = html;
} else {
let html = '<span class="error">' + (r.error || 'Deploy failed') + '</span><br>';
(r.data || []).forEach(s => {
html += (s.ok ? '<span class="status-ok">[OK]</span>' : '<span class="status-err">[FAIL]</span>') + ' ' + s.step + '<br>';
});
out.innerHTML = html;
}
loadE2EStatus();
}
async function testE2E() {
const out = document.getElementById('e2e-output');
out.style.display = 'block';
out.innerHTML = '<span class="info">Testing E2E tunnel...</span>';
const r = await apiPost('/api/e2e/test', {});
if (r.ok) {
out.innerHTML = '<span class="status-ok">E2E tunnel working!</span><br><pre>' + (r.data.output || '') + '</pre>';
} else {
out.innerHTML = '<span class="error">' + (r.error || 'Test failed') + '</span>';
}
}
loadProviders();
loadSettings();
loadE2EStatus();
</script>
{% endblock %}

View File

@@ -0,0 +1,161 @@
{% extends "base.html" %}
{% block title %}SMTP{% endblock %}
{% block content %}
<h1>[*] SMTP / Mail</h1>
<div class="toolbar">
<button class="btn" onclick="loadSmtpStatus()">Status</button>
<button class="btn" onclick="checkDNS()">Check DNS</button>
<button class="btn" onclick="flushQueue()">Flush Queue</button>
<button class="btn btn-warn" onclick="restartSmtp()">Restart Postfix</button>
<button class="btn" onclick="showTab('status')">Status</button>
<button class="btn" onclick="showTab('send')">Send Email</button>
<button class="btn" onclick="showTab('mass')">Mass Email (BCC)</button>
</div>
<!-- TAB: Status -->
<div id="tab-status">
<div class="grid grid-2">
<div class="card">
<div class="card-title">SMTP Status</div>
<div class="output" id="smtp-output"><span class="info">Loading...</span></div>
</div>
<div class="card">
<div class="card-title">DNS Records (SPF/DKIM/DMARC)</div>
<div class="output" id="dns-output"><span class="info">Click "Check DNS"</span></div>
</div>
</div>
<div class="card">
<div class="card-title">Quick Test</div>
<input type="email" id="test-email" placeholder="recipient@example.com" style="width:300px">
<button class="btn" onclick="sendTest()">Send Test</button>
</div>
</div>
<!-- TAB: Send Email -->
<div id="tab-send" style="display:none">
<div class="card">
<div class="card-title">Compose Email</div>
<label>From</label>
<input type="text" id="send-from" value="noreply@seteclabs.io" style="width:100%">
<label>To</label>
<input type="text" id="send-to" placeholder="recipient@example.com" style="width:100%">
<label>Subject</label>
<input type="text" id="send-subject" placeholder="Email subject" style="width:100%">
<label>Body</label>
<textarea id="send-body" rows="10" style="width:100%" placeholder="Type your message here..."></textarea>
<br><br>
<button class="btn" onclick="sendEmail()">Send Email</button>
</div>
</div>
<!-- TAB: Mass Email (BCC) -->
<div id="tab-mass" style="display:none">
<div class="card">
<div class="card-title">Mass Email (BCC Mode)</div>
<p style="color:#888;font-size:12px;margin-bottom:10px">
Each recipient gets their own individual email. No one can see other recipients' addresses.
</p>
<label>From</label>
<input type="text" id="mass-from" value="noreply@seteclabs.io" style="width:100%">
<label>Recipients (comma or newline separated)</label>
<textarea id="mass-to" rows="5" style="width:100%" placeholder="joe@example.com, sam@example.com, bob@example.com
or one per line:
joe@example.com
sam@example.com
bob@example.com"></textarea>
<label>Subject</label>
<input type="text" id="mass-subject" placeholder="Email subject" style="width:100%">
<label>Body</label>
<textarea id="mass-body" rows="10" style="width:100%" placeholder="Type your message here..."></textarea>
<br><br>
<div class="toolbar">
<button class="btn" onclick="previewMass()">Preview (count recipients)</button>
<button class="btn btn-warn" onclick="sendMass()">Send to All</button>
</div>
<div id="mass-preview" style="margin-top:10px;font-size:12px;color:#888"></div>
</div>
</div>
<div class="card">
<div class="card-title">Output</div>
<div class="output" id="output"><span class="info">Ready.</span></div>
</div>
{% endblock %}
{% block scripts %}
<script>
function showTab(tab) {
document.getElementById('tab-status').style.display = tab === 'status' ? '' : 'none';
document.getElementById('tab-send').style.display = tab === 'send' ? '' : 'none';
document.getElementById('tab-mass').style.display = tab === 'mass' ? '' : 'none';
}
// ── Status ──
async function loadSmtpStatus() {
const res = await apiGet('/api/smtp/status');
showResult(res, 'smtp-output');
}
async function checkDNS() {
const res = await apiGet('/api/smtp/dns-check');
showResult(res, 'dns-output');
}
async function flushQueue() {
const res = await apiPost('/api/smtp/flush');
showResult(res);
}
async function restartSmtp() {
const res = await apiPost('/api/smtp/restart');
showResult(res);
}
async function sendTest() {
const to = document.getElementById('test-email').value;
if (!to) { alert('Enter email address'); return; }
const res = await apiPost('/api/smtp/send-test', {to});
showResult(res);
}
// ── Send Email ──
async function sendEmail() {
const from = document.getElementById('send-from').value.trim();
const to = document.getElementById('send-to').value.trim();
const subject = document.getElementById('send-subject').value.trim();
const body = document.getElementById('send-body').value;
if (!to || !subject) { alert('Fill in To and Subject'); return; }
const res = await apiPost('/api/smtp/send', {from, to, subject, body});
showResult(res);
}
// ── Mass Email ──
function parseRecipients() {
const raw = document.getElementById('mass-to').value;
return raw.split(/[,\n]+/).map(e => e.trim()).filter(e => e && e.includes('@'));
}
function previewMass() {
const recipients = parseRecipients();
const el = document.getElementById('mass-preview');
el.innerHTML = `<span style="color:#00ff41">${recipients.length} recipients found:</span><br>` +
recipients.map(r => ` - ${escHtml(r)}`).join('<br>');
}
async function sendMass() {
const recipients = parseRecipients();
if (!recipients.length) { alert('Enter at least one recipient'); return; }
const from = document.getElementById('mass-from').value.trim();
const subject = document.getElementById('mass-subject').value.trim();
const body = document.getElementById('mass-body').value;
if (!subject) { alert('Enter a subject'); return; }
if (!confirm(`Send email to ${recipients.length} recipients individually (BCC mode)?`)) return;
const out = document.getElementById('output');
out.innerHTML = `<span class="info">Sending to ${recipients.length} recipients...</span>\n`;
const res = await apiPost('/api/smtp/send-mass', {from, recipients, subject, body});
showResult(res);
}
loadSmtpStatus();
</script>
{% endblock %}

View File

@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block title %}Terminal{% endblock %}
{% block content %}
<h1>[$] SSH Terminal</h1>
<div class="card">
<div class="card-title">Remote Shell (root@VPS)</div>
<div class="output" id="output" style="min-height:400px;max-height:700px"></div>
<div style="display:flex;margin-top:5px">
<span style="color:#00ff41;padding:6px">$</span>
<input type="text" id="cmd-input" placeholder="enter command..." style="flex:1"
onkeydown="if(event.key==='Enter')runCmd()"
autofocus>
<button class="btn" onclick="runCmd()">Run</button>
</div>
</div>
<div class="card">
<div class="card-title">Quick Commands</div>
<div class="toolbar">
<button class="btn" onclick="quickCmd('uptime')">uptime</button>
<button class="btn" onclick="quickCmd('free -h')">memory</button>
<button class="btn" onclick="quickCmd('df -h /')">disk</button>
<button class="btn" onclick="quickCmd('docker ps')">docker ps</button>
<button class="btn" onclick="quickCmd('systemctl status nginx')">nginx status</button>
<button class="btn" onclick="quickCmd('ufw status')">firewall</button>
<button class="btn" onclick="quickCmd('tail -20 /var/log/syslog')">syslog</button>
<button class="btn" onclick="quickCmd('last -10')">last logins</button>
<button class="btn" onclick="quickCmd('netstat -tlnp')">open ports</button>
<button class="btn" onclick="quickCmd('gitlab-ctl status')">gitlab status</button>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
const history = [];
let histIdx = -1;
const cmdInput = document.getElementById('cmd-input');
const output = document.getElementById('output');
cmdInput.addEventListener('keydown', (e) => {
if (e.key === 'ArrowUp') {
e.preventDefault();
if (histIdx < history.length - 1) { histIdx++; cmdInput.value = history[histIdx]; }
} else if (e.key === 'ArrowDown') {
e.preventDefault();
if (histIdx > 0) { histIdx--; cmdInput.value = history[histIdx]; }
else { histIdx = -1; cmdInput.value = ''; }
}
});
async function runCmd() {
const cmd = cmdInput.value.trim();
if (!cmd) return;
history.unshift(cmd);
histIdx = -1;
cmdInput.value = '';
output.innerHTML += '<span style="color:#888">$ ' + escHtml(cmd) + '</span>\n';
output.innerHTML += '<span class="info">running...</span>\n';
output.scrollTop = output.scrollHeight;
const res = await apiPost('/api/terminal/exec', {cmd});
// Remove the "running..." line
output.innerHTML = output.innerHTML.replace(/<span class="info">running\.\.\.<\/span>\n$/, '');
if (res.ok && res.data) {
if (res.data.stdout) output.innerHTML += escHtml(res.data.stdout);
if (res.data.stderr) output.innerHTML += '<span class="err">' + escHtml(res.data.stderr) + '</span>';
if (res.data.exit_code && res.data.exit_code !== 0)
output.innerHTML += '<span class="err">[exit: ' + res.data.exit_code + ']</span>\n';
} else {
output.innerHTML += '<span class="err">Error: ' + (res.error || 'unknown') + '</span>\n';
}
output.innerHTML += '\n';
output.scrollTop = output.scrollHeight;
}
function quickCmd(cmd) {
cmdInput.value = cmd;
runCmd();
}
</script>
{% endblock %}

View File

@@ -0,0 +1,516 @@
{% extends "base.html" %}
{% block title %}Setup Wizard{% endblock %}
{% block content %}
<h1>[+] Setup Wizard</h1>
<div id="wiz-steps" style="margin-bottom:15px;font-size:11px;color:#555">
<span id="ws-1" class="status-ok">[1] Terms</span> &rarr;
<span id="ws-2">[2] SSH Keys</span> &rarr;
<span id="ws-3">[3] VPS</span> &rarr;
<span id="ws-4">[4] API</span> &rarr;
<span id="ws-5">[5] Paths</span> &rarr;
<span id="ws-6">[6] Test</span>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Step 1: Terms / License -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-1" class="card">
<div class="card-title">License Agreement &amp; Terms of Use</div>
<div style="font-size:12px;line-height:1.6;max-height:400px;overflow-y:auto;padding:10px;background:#000;border:1px solid #333;margin-bottom:15px">
<p style="color:#ff4444;margin-bottom:10px"><strong>IMPORTANT - READ BEFORE CONTINUING</strong></p>
<p style="margin-bottom:10px"><strong style="color:#ff4444">1. RESTRICTED USE LICENSE</strong><br>
This software is licensed for use by <strong>private individuals, independent security
researchers, and non-governmental organizations ONLY</strong>. By using this software you
affirm that you are not acting on behalf of, employed by, contracted by, or otherwise
affiliated with any:<br>
&bull; <strong style="color:#ff4444">Law enforcement agency</strong> (local, state, federal, or international)<br>
&bull; <strong style="color:#ff4444">Government agency or department</strong> (civilian or military)<br>
&bull; <strong style="color:#ff4444">Intelligence service</strong> (domestic or foreign)<br>
&bull; <strong style="color:#ff4444">Government contractor</strong> performing work for any of the above<br>
<br>
Use of this software by any of the above entities or their agents is <strong>strictly
prohibited</strong> 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.</p>
<p style="margin-bottom:10px"><strong style="color:#88ff88">2. NO WARRANTY</strong><br>
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.</p>
<p style="margin-bottom:10px"><strong style="color:#88ff88">3. NO GUARANTEE OF SUPPORT</strong><br>
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.</p>
<p style="margin-bottom:10px"><strong style="color:#88ff88">4. LIMITATION OF LIABILITY</strong><br>
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.</p>
<p style="margin-bottom:10px"><strong style="color:#ff4444">5. IF YOU PAID FOR THIS SOFTWARE, YOU WERE SCAMMED</strong><br>
SETEC LABS Manager is <strong>100% free and open source</strong>. If someone charged you
money for this application, you were ripped off and likely received a version bundled
with malware. Delete it immediately.</p>
<p style="margin-bottom:10px"><strong style="color:#88ff88">6. OFFICIAL DOWNLOAD SOURCES</strong><br>
Only download SETEC applications from these trusted sources:<br>
&bull; <span style="color:#00ff41">repo.seteclabs.io</span> (Official Gitea repository)<br>
&bull; <span style="color:#00ff41">github.com/DigiJEth</span> (Official GitHub mirror)<br>
Any other source is unauthorized and potentially dangerous.</p>
<p style="margin-bottom:10px"><strong style="color:#88ff88">7. ROOT ACCESS WARNING</strong><br>
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.</p>
<p style="color:#888;font-size:10px;margin-top:15px;border-top:1px solid #333;padding-top:10px">
SETEC LABS Manager &bull; Free Software &bull; darkHal Group &bull; For the people, not the state.</p>
</div>
<div style="margin-bottom:10px">
<label style="display:inline;cursor:pointer">
<input type="checkbox" id="tos-accept" onchange="tosChanged()" style="margin-right:8px">
<span style="color:#ffaa00">I have read and accept these terms and I am not affiliated with any government or law enforcement entity</span>
</label>
</div>
<button class="btn" id="btn-tos-next" onclick="acceptTOS()" disabled style="opacity:0.3">Next &rarr;</button>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Step 2: SSH Key Selection/Generation -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-2" class="card" style="display:none">
<div class="card-title">SSH Key Setup</div>
<p style="font-size:12px;color:#888;margin-bottom:15px">
SETEC Manager connects to your VPS via SSH using key-based authentication.
Do you already have SSH keys generated?
</p>
<div style="margin-bottom:15px">
<button class="btn" id="btn-has-keys" onclick="sshKeyChoice('yes')" style="padding:10px 20px">
Yes, I have SSH keys
</button>
<button class="btn" id="btn-no-keys" onclick="sshKeyChoice('no')" style="padding:10px 20px">
No, I need to create them
</button>
</div>
<!-- YES: User has keys -->
<div id="ssh-has-keys" style="display:none">
<label>SSH Private Key Path</label>
<input type="text" id="w-key" style="width:100%" placeholder="C:/keys/setec">
<div style="font-size:10px;color:#555;margin:2px 3px 5px">
Enter the full path to your <strong style="color:#888">private</strong> key file (not the .pub file).
</div>
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
Common locations:<br>
<span style="color:#00ff41;line-height:1.8">
&bull; C:/Users/YourName/.ssh/id_ed25519<br>
&bull; C:/Users/YourName/.ssh/id_rsa<br>
&bull; C:/keys/setec<br>
&bull; ~/.ssh/id_ed25519 (Linux/Mac)
</span>
</div>
<div style="margin-top:10px">
<button class="btn" onclick="goStep(1)">&larr; Back</button>
<button class="btn" onclick="saveKeyAndContinue()">Save &amp; Continue &rarr;</button>
</div>
</div>
<!-- NO: User needs keys -->
<div id="ssh-no-keys" style="display:none">
<p style="font-size:12px;color:#ffaa00;margin-bottom:10px">
No problem! Select your VPS hosting provider below for a step-by-step guide.
</p>
<label>Select your VPS host</label>
<select id="w-ssh-host" style="width:100%" onchange="sshHostChanged()">
<option value="">-- Select Host --</option>
<option value="hostinger">Hostinger</option>
<option value="digitalocean">DigitalOcean</option>
<option value="vultr">Vultr</option>
<option value="linode">Linode (Akamai)</option>
<option value="hetzner">Hetzner</option>
<option value="ovh">OVH / OVHcloud</option>
<option value="aws">AWS (EC2)</option>
<option value="contabo">Contabo</option>
<option value="other">Other / Self-Hosted</option>
</select>
<div id="ssh-host-guide" style="display:none;margin-top:15px;padding:12px;background:#000;border:1px solid #333;font-size:11px;line-height:1.8"></div>
<div id="ssh-generic-guide" style="display:none;margin-top:15px;padding:12px;background:#000;border:1px solid #333">
<p style="color:#88ff88;font-size:12px;margin-bottom:8px"><strong>Generate SSH Keys (any platform)</strong></p>
<p style="font-size:11px;color:#888;margin-bottom:8px">Open a terminal and run:</p>
<div style="font-size:10px;margin-bottom:10px">
<p style="color:#888;margin-bottom:3px">1. Generate key pair:</p>
<code style="color:#00ff41">ssh-keygen -t ed25519 -f C:/keys/setec -N ""</code>
</div>
<div style="font-size:10px;margin-bottom:10px">
<p style="color:#888;margin-bottom:3px">2. Copy the public key to your server:</p>
<code style="color:#00ff41">ssh-copy-id -i C:/keys/setec.pub root@YOUR_SERVER_IP</code>
</div>
<div style="font-size:10px;margin-bottom:10px">
<p style="color:#888;margin-bottom:3px">3. Test the connection:</p>
<code style="color:#00ff41">ssh -i C:/keys/setec root@YOUR_SERVER_IP</code>
</div>
<p style="font-size:10px;color:#555;margin-top:8px">Your private key will be at <span style="color:#00ff41">C:/keys/setec</span></p>
</div>
<div style="margin-top:15px">
<label>Once your keys are ready, enter the private key path:</label>
<input type="text" id="w-key-new" style="width:100%" placeholder="C:/keys/setec" value="C:/keys/setec">
</div>
<div style="margin-top:10px">
<button class="btn" onclick="goStep(1)">&larr; Back</button>
<button class="btn" onclick="saveNewKeyAndContinue()">Save &amp; Continue &rarr;</button>
</div>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Step 3: VPS Connection -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-3" class="card" style="display:none">
<div class="card-title">VPS Connection Setup</div>
<label>Server IP Address</label>
<input type="text" id="w-host" style="width:100%" placeholder="e.g. 192.168.1.100">
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
Find your VPS IP in your hosting provider's control panel.
</div>
<label>SSH Username</label>
<input type="text" id="w-user" style="width:100%" placeholder="root" value="root">
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
<strong style="color:#888">Recommended:</strong> Use <span style="color:#00ff41">root</span> or create a sudo user:<br>
<code style="color:#00ff41;font-size:10px">adduser setecadmin && usermod -aG sudo setecadmin</code><br>
<span style="color:#ffaa00">Important:</span> Disable password login after setting up SSH keys:<br>
<code style="color:#00ff41;font-size:10px">sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config && systemctl restart sshd</code>
</div>
<label>SSH Port</label>
<input type="number" id="w-port" style="width:100%" placeholder="2222" value="2222">
<div style="font-size:10px;color:#555;margin:2px 3px 10px">
<strong style="color:#ffaa00">We strongly recommend port 2222</strong> instead of default 22 to reduce brute-force attacks.<br>
<code style="color:#00ff41;font-size:10px">sed -i 's/^#*Port .*/Port 2222/' /etc/ssh/sshd_config && ufw allow 2222/tcp && systemctl restart sshd</code><br>
<span style="color:#ff4444">Do NOT close your current session until you verify the new port works!</span>
</div>
<div style="margin-top:10px">
<button class="btn" onclick="goStep(2)">&larr; Back</button>
<button class="btn" onclick="saveVPS()">Save &amp; Continue &rarr;</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Step 4: API Setup -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-4" class="card" style="display:none">
<div class="card-title">DNS API Setup</div>
<label>Domain</label>
<input type="text" id="w-domain" style="width:100%" placeholder="example.com">
<div style="font-size:10px;color:#555;margin:2px 3px 5px">Your primary domain name managed by this panel.</div>
<label>DNS Provider</label>
<select id="w-provider" style="width:100%" onchange="wizProviderChanged()">
<option value="">-- Select Provider --</option>
</select>
<div id="w-provider-notes" style="font-size:11px;color:#ffaa00;margin:5px 3px;min-height:20px"></div>
<label id="w-lbl-apikey">API Key</label>
<input type="text" id="w-apikey" style="width:100%" placeholder="Enter API key">
<div id="w-provider-docs" style="font-size:11px;margin:5px 3px"></div>
<div id="w-provider-help" style="font-size:10px;color:#555;margin:2px 3px 10px;display:none">
<div id="w-help-content"></div>
</div>
<div style="margin-top:10px">
<button class="btn" onclick="goStep(3)">&larr; Back</button>
<button class="btn" onclick="saveAPI()">Save &amp; Continue &rarr;</button>
</div>
</div>
<!-- ═══════════════════════════════════════════════════════════════ -->
<!-- Step 5: Paths -->
<!-- ═══════════════════════════════════════════════════════════════ -->
<div id="step-5" class="card" style="display:none">
<div class="card-title">Web Root &amp; Compose Path</div>
<label>Web Root Directory</label>
<input type="text" id="w-webroot" style="width:100%" placeholder="/var/www" value="/var/www">
<div style="font-size:10px;color:#555;margin:2px 3px 10px">Default: <span style="color:#00ff41">/var/www</span></div>
<label>Docker Compose Path</label>
<input type="text" id="w-compose" style="width:100%" placeholder="/opt/seteclabs/docker-compose.yml">
<div style="font-size:10px;color:#00ff41;margin:0 3px 5px;line-height:1.8">
&bull; /opt/seteclabs/docker-compose.yml<br>
&bull; /root/docker-compose.yml<br>
&bull; /srv/docker-compose.yml
</div>
<div style="margin-top:10px">
<button class="btn" onclick="goStep(4)">&larr; Back</button>
<button class="btn" onclick="savePaths()">Save &amp; Finish Setup &rarr;</button>
</div>
</div>
<!-- Step 6: Test (modal trigger) -->
<div id="step-6" style="display:none"></div>
<!-- Modal overlay -->
<div id="wiz-modal" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.85);z-index:999;display:none;align-items:center;justify-content:center">
<div style="background:#111;border:1px solid #00ff41;padding:25px;max-width:500px;width:90%;max-height:80vh;overflow-y:auto">
<div id="modal-title" style="font-size:14px;color:#88ff88;margin-bottom:15px;border-bottom:1px solid #333;padding-bottom:8px">Setup Complete</div>
<div id="modal-body" style="font-size:12px;line-height:1.6"></div>
<div id="modal-buttons" style="margin-top:15px"></div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let wizProviders = [];
let currentStep = 1;
// ── Step navigation ─────────────────────────────────────────────
function goStep(n) {
for (let i = 1; i <= 6; i++) {
const el = document.getElementById('step-' + i);
if (el) el.style.display = (i === n) ? 'block' : 'none';
const ws = document.getElementById('ws-' + i);
if (ws) ws.className = (i <= n) ? 'status-ok' : '';
}
currentStep = n;
}
function tosChanged() {
const btn = document.getElementById('btn-tos-next');
if (document.getElementById('tos-accept').checked) {
btn.disabled = false; btn.style.opacity = '1';
} else {
btn.disabled = true; btn.style.opacity = '0.3';
}
}
// ── TOS acceptance ──────────────────────────────────────────────
async function acceptTOS() {
const res = await fetch('/api/wizard/accept-tos', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({})
}).then(r => r.json());
if (res.ok) {
goStep(2);
} else {
alert('Error saving TOS acceptance: ' + (res.error || 'Unknown'));
}
}
// ── Provider help ────────────────────────────────────────────────
const providerHelp = {
hostinger: 'Log in to <strong>hPanel</strong> &rarr; click your profile icon &rarr; <strong>API Keys</strong> &rarr; Create new key with DNS permissions.',
cloudflare: 'Go to <strong>dash.cloudflare.com</strong> &rarr; My Profile &rarr; <strong>API Tokens</strong> &rarr; Create Token &rarr; use "Edit zone DNS" template.',
digitalocean: 'Go to <strong>cloud.digitalocean.com</strong> &rarr; API &rarr; <strong>Tokens</strong> &rarr; Generate New Token with read+write scope.',
vultr: 'Go to <strong>my.vultr.com</strong> &rarr; Account &rarr; <strong>API</strong> &rarr; Enable API and copy the key.',
linode: 'Go to <strong>cloud.linode.com</strong> &rarr; My Profile &rarr; <strong>API Tokens</strong> &rarr; Create Personal Access Token.',
godaddy: 'Go to <strong>developer.godaddy.com</strong> &rarr; API Keys &rarr; Create New API Key. Format: <span style="color:#00ff41">key:secret</span>.',
namecheap: 'Go to <strong>namecheap.com</strong> &rarr; Profile &rarr; Tools &rarr; <strong>API Access</strong>. Whitelist your IP.',
hetzner: 'Go to <strong>dns.hetzner.com</strong> &rarr; API Tokens &rarr; Create new token.',
ovh: 'Go to <strong>api.ovh.com/createApp</strong>. You need Application Key, Application Secret, and Consumer Key.',
aws_route53: 'In <strong>AWS IAM Console</strong>, create access key with Route53 permissions. Format: <span style="color:#00ff41">ACCESS_KEY:SECRET</span>.'
};
async function loadWizProviders() {
const res = await apiGet('/api/hosting/providers');
if (!res.ok) return;
wizProviders = res.data;
const sel = document.getElementById('w-provider');
wizProviders.forEach(p => {
const opt = document.createElement('option');
opt.value = p.id;
opt.textContent = p.name;
sel.appendChild(opt);
});
}
function wizProviderChanged() {
const id = document.getElementById('w-provider').value;
const p = wizProviders.find(x => x.id === id);
if (p) {
document.getElementById('w-lbl-apikey').textContent = p.api_key_label || 'API Key';
document.getElementById('w-provider-notes').textContent = p.notes || '';
document.getElementById('w-provider-docs').innerHTML = p.docs ?
'Docs: <a href="' + escHtml(p.docs) + '" target="_blank" style="color:#00ff41">' + escHtml(p.docs) + '</a>' : '';
const helpDiv = document.getElementById('w-provider-help');
const helpContent = document.getElementById('w-help-content');
if (providerHelp[id]) {
helpContent.innerHTML = '<strong style="color:#88ff88">How to get your key:</strong><br>' + providerHelp[id];
helpDiv.style.display = 'block';
} else { helpDiv.style.display = 'none'; }
} else {
document.getElementById('w-lbl-apikey').textContent = 'API Key';
document.getElementById('w-provider-notes').textContent = '';
document.getElementById('w-provider-docs').innerHTML = '';
document.getElementById('w-provider-help').style.display = 'none';
}
}
// ── SSH host guides ─────────────────────────────────────────────
const sshHostGuides = {
hostinger: { name: 'Hostinger', url: 'https://support.hostinger.com/en/articles/1583522-how-to-generate-ssh-keys', steps: 'Log in to <strong>hPanel</strong> &rarr; VPS &rarr; Settings &rarr; <strong>SSH Keys</strong> &rarr; Add SSH Key.' },
digitalocean: { name: 'DigitalOcean', url: 'https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/', steps: 'Go to <strong>Settings</strong> &rarr; <strong>Security</strong> &rarr; SSH Keys &rarr; Add SSH Key.' },
vultr: { name: 'Vultr', url: 'https://docs.vultr.com/how-do-i-generate-ssh-keys', steps: 'Go to <strong>Account</strong> &rarr; <strong>SSH Keys</strong> &rarr; Add SSH Key.' },
linode: { name: 'Linode (Akamai)', url: 'https://www.linode.com/docs/guides/use-public-key-authentication-with-ssh/', steps: 'Go to <strong>Profile</strong> &rarr; <strong>SSH Keys</strong> &rarr; Add SSH Key.' },
hetzner: { name: 'Hetzner', url: 'https://docs.hetzner.com/cloud/servers/getting-started/connecting-to-the-server/', steps: 'Go to <strong>Security</strong> &rarr; <strong>SSH Keys</strong> in Hetzner Cloud Console.' },
ovh: { name: 'OVH', url: 'https://help.ovhcloud.com/csm/en-dedicated-servers-creating-ssh-keys?id=kb_article_view&sysparm_article=KB0047697', steps: 'In OVHcloud Control Panel &rarr; <strong>Public Cloud</strong> &rarr; <strong>SSH Keys</strong>.' },
aws: { name: 'AWS (EC2)', url: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html', steps: 'AWS Console &rarr; EC2 &rarr; <strong>Key Pairs</strong> &rarr; Create or Import.' },
contabo: { name: 'Contabo', url: 'https://contabo.com/blog/establishing-connection-server-ssh/', steps: 'Generate keys locally, copy manually with <code style="color:#00ff41">ssh-copy-id</code>.' },
other: { name: 'Other', url: '', steps: '' }
};
function sshKeyChoice(choice) {
document.getElementById('ssh-has-keys').style.display = (choice === 'yes') ? 'block' : 'none';
document.getElementById('ssh-no-keys').style.display = (choice === 'no') ? 'block' : 'none';
document.getElementById('btn-has-keys').style.background = (choice === 'yes') ? '#00ff41' : '';
document.getElementById('btn-has-keys').style.color = (choice === 'yes') ? '#000' : '';
document.getElementById('btn-no-keys').style.background = (choice === 'no') ? '#00ff41' : '';
document.getElementById('btn-no-keys').style.color = (choice === 'no') ? '#000' : '';
}
function sshHostChanged() {
const id = document.getElementById('w-ssh-host').value;
const guideDiv = document.getElementById('ssh-host-guide');
const genericDiv = document.getElementById('ssh-generic-guide');
if (!id) { guideDiv.style.display = 'none'; genericDiv.style.display = 'none'; return; }
const host = sshHostGuides[id];
genericDiv.style.display = 'block';
if (host && (host.url || host.steps)) {
let html = '<p style="color:#88ff88;font-size:12px;margin-bottom:8px"><strong>' + escHtml(host.name) + ' SSH Key Guide</strong></p>';
if (host.steps) html += '<p style="font-size:11px;color:#888;margin-bottom:8px">' + host.steps + '</p>';
if (host.url) html += '<p style="margin-top:8px"><a href="' + escHtml(host.url) + '" target="_blank" style="color:#00ff41">' + escHtml(host.url) + '</a></p>';
guideDiv.innerHTML = html;
guideDiv.style.display = 'block';
} else { guideDiv.style.display = 'none'; }
}
function saveKeyAndContinue() {
if (!document.getElementById('w-key').value) { alert('Enter SSH private key path.'); return; }
goStep(3);
}
function saveNewKeyAndContinue() {
const k = document.getElementById('w-key-new').value;
if (!k) { alert('Enter SSH private key path.'); return; }
document.getElementById('w-key').value = k;
goStep(3);
}
function getKeyPath() {
return document.getElementById('w-key').value || document.getElementById('w-key-new').value || '';
}
async function saveVPS() {
const body = { vps_host: document.getElementById('w-host').value, vps_user: document.getElementById('w-user').value,
vps_port: parseInt(document.getElementById('w-port').value) || 22, ssh_key_path: getKeyPath() };
if (!body.vps_host) { alert('Enter server IP.'); return; }
if (!body.ssh_key_path) { alert('Go back and enter SSH key path.'); return; }
const res = await apiPost('/api/settings', body);
if (res.ok) goStep(4); else alert('Error: ' + (res.error || 'Unknown'));
}
async function saveAPI() {
const body = { domain: document.getElementById('w-domain').value, hosting_provider: document.getElementById('w-provider').value,
hostinger_api_key: document.getElementById('w-apikey').value };
if (!body.domain) { alert('Enter your domain.'); return; }
const res = await apiPost('/api/settings', body);
if (res.ok) goStep(5); else alert('Error: ' + (res.error || 'Unknown'));
}
async function savePaths() {
const body = { web_root: document.getElementById('w-webroot').value || '/var/www',
compose_path: document.getElementById('w-compose').value, setup_complete: true };
const res = await apiPost('/api/settings', body);
if (res.ok) showTestModal(); else alert('Error: ' + (res.error || 'Unknown'));
}
// ── Test modal (same as before) ─────────────────────────────────
function showTestModal() {
document.getElementById('wiz-modal').style.display = 'flex';
document.getElementById('modal-title').textContent = 'Setup Complete!';
document.getElementById('modal-body').innerHTML =
'<p style="margin-bottom:10px">Your settings have been saved.</p>' +
'<p>Would you like to test the connection?</p>';
document.getElementById('modal-buttons').innerHTML =
'<button class="btn" onclick="runTest()">Yes, Test Connection</button> ' +
'<button class="btn" onclick="closeModal()">Skip</button>';
}
async function runTest() {
document.getElementById('modal-title').textContent = 'Testing Connection...';
document.getElementById('modal-body').innerHTML = '<span style="color:#00ff41">Connecting via SSH...</span>';
document.getElementById('modal-buttons').innerHTML = '';
const sshRes = await apiGet('/api/wizard/test');
if (sshRes.ok && sshRes.data) {
const d = sshRes.data;
if (d.ssh_ok) {
let html = '<p style="color:#00ff41;margin-bottom:10px"><strong>SSH: SUCCESS</strong></p>';
html += '<div style="background:#000;padding:8px;border:1px solid #333;font-size:11px;margin-bottom:10px">' + escHtml(d.ssh_output || '') + '</div>';
if (d.api_ok) html += '<p style="color:#00ff41"><strong>DNS API: SUCCESS</strong></p>';
else if (d.api_error) html += '<p style="color:#ffaa00"><strong>DNS API: ' + escHtml(d.api_error) + '</strong></p>';
document.getElementById('modal-title').textContent = 'Connection Successful!';
document.getElementById('modal-body').innerHTML = html;
document.getElementById('modal-buttons').innerHTML =
'<button class="btn" onclick="window.location.href=\'/\'">Go to Dashboard</button>';
} else { showTestFailed(d.ssh_error || d.error || 'Connection failed'); }
} else { showTestFailed(sshRes.error || 'Connection failed'); }
}
function showTestFailed(error) {
document.getElementById('modal-title').textContent = 'Connection Failed';
document.getElementById('modal-body').innerHTML =
'<p style="color:#ff4444;margin-bottom:10px"><strong>Connection Failed</strong></p>' +
'<div style="background:#000;padding:8px;border:1px solid #ff4444;font-size:11px;margin-bottom:10px;color:#ff4444;white-space:pre-wrap">' + escHtml(error) + '</div>' +
'<p style="color:#ffaa00;margin-bottom:8px">Check:</p>' +
'<ul style="font-size:11px;color:#888;margin-left:15px;line-height:1.8">' +
'<li>Server IP is correct and VPS is running</li>' +
'<li>SSH port is open and correct</li>' +
'<li>SSH key exists at the specified path</li>' +
'<li>Public key is in ~/.ssh/authorized_keys on server</li></ul>';
document.getElementById('modal-buttons').innerHTML =
'<button class="btn" onclick="closeModal()">Close &amp; Fix Settings</button>';
}
function closeModal() { document.getElementById('wiz-modal').style.display = 'none'; }
// ── Prefill ─────────────────────────────────────────────────────
async function prefillWizard() {
const res = await apiGet('/api/settings');
if (!res.ok) return;
const d = res.data;
if (d.vps_host) document.getElementById('w-host').value = d.vps_host;
if (d.vps_user) document.getElementById('w-user').value = d.vps_user;
if (d.vps_port) document.getElementById('w-port').value = d.vps_port;
if (d.ssh_key_path) {
document.getElementById('w-key').value = d.ssh_key_path;
document.getElementById('w-key-new').value = d.ssh_key_path;
}
if (d.domain) document.getElementById('w-domain').value = d.domain;
if (d.hostinger_api_key) document.getElementById('w-apikey').value = d.hostinger_api_key;
if (d.web_root) document.getElementById('w-webroot').value = d.web_root;
if (d.compose_path) document.getElementById('w-compose').value = d.compose_path;
if (d.hosting_provider) {
document.getElementById('w-provider').value = d.hosting_provider;
wizProviderChanged();
}
}
loadWizProviders().then(() => prefillWizard());
</script>
{% endblock %}

53
setec-web/wsgi.py Normal file
View File

@@ -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()