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:
55
.gitignore
vendored
Normal file
55
.gitignore
vendored
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# ── Not part of the manager ──
|
||||||
|
site/
|
||||||
|
insurrect-us/
|
||||||
|
projects/
|
||||||
|
services/
|
||||||
|
setec-mgr/
|
||||||
|
setec-tools/
|
||||||
|
setup-vps.sh
|
||||||
|
fix-remaining.sh
|
||||||
|
ssh_cmd.py
|
||||||
|
|
||||||
|
# ── Resources (keep logo only) ──
|
||||||
|
resources/*
|
||||||
|
!resources/setec_labs_logo.svg
|
||||||
|
|
||||||
|
# ── Claude Code config (contains approved commands with secrets) ──
|
||||||
|
.claude/
|
||||||
|
|
||||||
|
# ── Python ──
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.egg-info/
|
||||||
|
dist/
|
||||||
|
build/
|
||||||
|
*.egg
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
env/
|
||||||
|
|
||||||
|
# ── Config (contains user secrets) ──
|
||||||
|
config.json
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# ── Logs ──
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# ── Compiled Go agent binary ──
|
||||||
|
setec-web/agent/setec-agent
|
||||||
|
|
||||||
|
# ── Node.js ──
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# ── IDE ──
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# ── OS ──
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
desktop.ini
|
||||||
11
manager.bat
Normal file
11
manager.bat
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0setec-web"
|
||||||
|
echo ===================================
|
||||||
|
echo SETEC LABS Manager v2.0
|
||||||
|
echo http://localhost:5000
|
||||||
|
echo ===================================
|
||||||
|
echo.
|
||||||
|
pip install flask paramiko requests >nul 2>&1
|
||||||
|
start http://localhost:5000
|
||||||
|
python app.py
|
||||||
|
pause
|
||||||
7
reload.bat
Normal file
7
reload.bat
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@echo off
|
||||||
|
taskkill /f /im python.exe >nul 2>&1
|
||||||
|
timeout /t 1 /nobreak >nul
|
||||||
|
cd /d "%~dp0setec-web"
|
||||||
|
start http://localhost:5000
|
||||||
|
python app.py
|
||||||
|
pause
|
||||||
67
resources/setec_labs_logo.svg
Normal file
67
resources/setec_labs_logo.svg
Normal 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 |
3
setec-web/agent/go.mod
Normal file
3
setec-web/agent/go.mod
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
module setec-agent
|
||||||
|
|
||||||
|
go 1.21
|
||||||
189
setec-web/agent/main.go
Normal file
189
setec-web/agent/main.go
Normal 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
141
setec-web/aide.py
Normal 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
2724
setec-web/app.py
Normal file
File diff suppressed because it is too large
Load Diff
69
setec-web/audit.py
Normal file
69
setec-web/audit.py
Normal 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
179
setec-web/backup.py
Normal 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
106
setec-web/chkrootkit.py
Normal 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
187
setec-web/clamav.py
Normal 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
61
setec-web/config.py
Normal 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
175
setec-web/cowrie.py
Normal 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
134
setec-web/csf.py
Normal 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
464
setec-web/ddos.py
Normal 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
437
setec-web/detector.py
Normal 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
110
setec-web/dns_client.py
Normal 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
709
setec-web/docker_store.py
Normal 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
76
setec-web/e2e.py
Normal 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
96
setec-web/firewalld.py
Normal 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
220
setec-web/hardening.py
Normal 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
105
setec-web/hosting.py
Normal 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
209
setec-web/iptables.py
Normal 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
98
setec-web/lynis.py
Normal 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
188
setec-web/modsecurity.py
Normal 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
359
setec-web/monitoring.py
Normal 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
110
setec-web/nftables.py
Normal 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
113
setec-web/ossec.py
Normal 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.'"
|
||||||
|
)
|
||||||
4
setec-web/requirements.txt
Normal file
4
setec-web/requirements.txt
Normal 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
152
setec-web/rkhunter.py
Normal 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
13
setec-web/run.bat
Normal 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
101
setec-web/sanitize.py
Normal 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
141
setec-web/sec_updates.py
Normal 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
242
setec-web/security_apps.py
Normal 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
126
setec-web/ssh_client.py
Normal 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
105
setec-web/ssl_audit.py
Normal 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"
|
||||||
|
)
|
||||||
67
setec-web/static/setec_labs_logo.svg
Normal file
67
setec-web/static/setec_labs_logo.svg
Normal 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 |
299
setec-web/templates/base.html
Normal file
299
setec-web/templates/base.html
Normal 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">[<]</span> Front Page
|
||||||
|
</a>
|
||||||
|
<a href="/security" class="{% if request.endpoint == 'security_page' %}active{% endif %}">
|
||||||
|
<span class="icon">[&]</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,'&').replace(/</g,'<').replace(/>/g,'>');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
222
setec-web/templates/configs.html
Normal file
222
setec-web/templates/configs.html
Normal 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 %}
|
||||||
54
setec-web/templates/dashboard.html
Normal file
54
setec-web/templates/dashboard.html
Normal 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 %}
|
||||||
140
setec-web/templates/detect.html
Normal file
140
setec-web/templates/detect.html
Normal 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 %}
|
||||||
101
setec-web/templates/dns.html
Normal file
101
setec-web/templates/dns.html
Normal 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 %}
|
||||||
334
setec-web/templates/docker.html
Normal file
334
setec-web/templates/docker.html
Normal 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/<name> 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/<name>/, 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 %}
|
||||||
609
setec-web/templates/docs.html
Normal file
609
setec-web/templates/docs.html
Normal 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 — 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> — Read and accept the disclaimer.</li>
|
||||||
|
<li><strong style="color:#88ff88">SSH Keys</strong> — Select existing keys or generate new ones with host-specific guidance.</li>
|
||||||
|
<li><strong style="color:#88ff88">VPS Connection</strong> — Enter your server IP, SSH username, port (2222 recommended), and key path.</li>
|
||||||
|
<li><strong style="color:#88ff88">DNS API</strong> — Select your hosting provider, enter your domain and API key.</li>
|
||||||
|
<li><strong style="color:#88ff88">Paths</strong> — Set web root and Docker Compose file location.</li>
|
||||||
|
<li><strong style="color:#88ff88">Connection Test</strong> — 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> — Hostname, OS, kernel, uptime</li>
|
||||||
|
<li><strong>Resource Usage</strong> — CPU, RAM, disk, swap</li>
|
||||||
|
<li><strong>Network</strong> — Active connections, listening ports</li>
|
||||||
|
<li><strong>Services</strong> — 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> — Firewall activity overview and monitoring</li>
|
||||||
|
<li><strong>UFW</strong> — Simplified firewall rule management</li>
|
||||||
|
<li><strong>iptables</strong> — Advanced packet filtering rules</li>
|
||||||
|
<li><strong>nftables</strong> — Modern netfilter framework management</li>
|
||||||
|
<li><strong>firewalld</strong> — Zone-based firewall management</li>
|
||||||
|
<li><strong>CSF</strong> — ConfigServer Security & Firewall</li>
|
||||||
|
<li><strong>Migration</strong> — 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> — Disable root login, enforce key auth, change port</li>
|
||||||
|
<li><strong>Kernel Hardening</strong> — Sysctl tweaks for network and memory protection</li>
|
||||||
|
<li><strong>Auto Updates</strong> — Enable unattended-upgrades for security patches</li>
|
||||||
|
<li><strong>.sec Patch System</strong> — 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> — Host, user, port, SSH key path</li>
|
||||||
|
<li><strong>Hosting Provider API</strong> — Provider selection, API key, documentation links</li>
|
||||||
|
<li><strong>Domain & Paths</strong> — 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 & Tips</h2>
|
||||||
|
<ul style="margin-left:15px;line-height:2">
|
||||||
|
<li>All actions use AJAX — 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 — 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 → Profile → API Keys → 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 → VPS → Settings → SSH Keys → 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 → My Profile → API Tokens → Create Token → "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 → API → Tokens → 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 → Security → SSH Keys → 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 → Account → API → Enable API → 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 → SSH Keys → 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 → My Profile → API Tokens → 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 → SSH Keys → 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 → API Keys → 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 → Tools → API Access → 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 → API Tokens → 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 → Security → SSH Keys → 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 → Public Cloud → SSH Keys → 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 → Users → 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 → Key Pairs → 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> or <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 — 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 — 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 — 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 — they share the same backend</li>
|
||||||
|
<li>Use the Migration tabs (UFW↔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 — 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 %}
|
||||||
168
setec-web/templates/fail2ban.html
Normal file
168
setec-web/templates/fail2ban.html
Normal 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 %}
|
||||||
93
setec-web/templates/files.html
Normal file
93
setec-web/templates/files.html
Normal 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 %}
|
||||||
990
setec-web/templates/firewall.html
Normal file
990
setec-web/templates/firewall.html
Normal 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 %}
|
||||||
182
setec-web/templates/frontpage.html
Normal file
182
setec-web/templates/frontpage.html
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Front Page Editor{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>[<] 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' ? '</>' : 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 %}
|
||||||
107
setec-web/templates/nginx.html
Normal file
107
setec-web/templates/nginx.html
Normal 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 %}
|
||||||
1598
setec-web/templates/security.html
Normal file
1598
setec-web/templates/security.html
Normal file
File diff suppressed because it is too large
Load Diff
216
setec-web/templates/settings.html
Normal file
216
setec-web/templates/settings.html
Normal 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 += ' | 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 %}
|
||||||
161
setec-web/templates/smtp.html
Normal file
161
setec-web/templates/smtp.html
Normal 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 %}
|
||||||
83
setec-web/templates/terminal.html
Normal file
83
setec-web/templates/terminal.html
Normal 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 %}
|
||||||
516
setec-web/templates/wizard.html
Normal file
516
setec-web/templates/wizard.html
Normal 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> →
|
||||||
|
<span id="ws-2">[2] SSH Keys</span> →
|
||||||
|
<span id="ws-3">[3] VPS</span> →
|
||||||
|
<span id="ws-4">[4] API</span> →
|
||||||
|
<span id="ws-5">[5] Paths</span> →
|
||||||
|
<span id="ws-6">[6] Test</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<!-- Step 1: Terms / License -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="step-1" class="card">
|
||||||
|
<div class="card-title">License Agreement & 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>
|
||||||
|
• <strong style="color:#ff4444">Law enforcement agency</strong> (local, state, federal, or international)<br>
|
||||||
|
• <strong style="color:#ff4444">Government agency or department</strong> (civilian or military)<br>
|
||||||
|
• <strong style="color:#ff4444">Intelligence service</strong> (domestic or foreign)<br>
|
||||||
|
• <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>
|
||||||
|
• <span style="color:#00ff41">repo.seteclabs.io</span> (Official Gitea repository)<br>
|
||||||
|
• <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 • Free Software • darkHal Group • 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 →</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">
|
||||||
|
• C:/Users/YourName/.ssh/id_ed25519<br>
|
||||||
|
• C:/Users/YourName/.ssh/id_rsa<br>
|
||||||
|
• C:/keys/setec<br>
|
||||||
|
• ~/.ssh/id_ed25519 (Linux/Mac)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<button class="btn" onclick="goStep(1)">← Back</button>
|
||||||
|
<button class="btn" onclick="saveKeyAndContinue()">Save & Continue →</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)">← Back</button>
|
||||||
|
<button class="btn" onclick="saveNewKeyAndContinue()">Save & Continue →</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)">← Back</button>
|
||||||
|
<button class="btn" onclick="saveVPS()">Save & Continue →</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)">← Back</button>
|
||||||
|
<button class="btn" onclick="saveAPI()">Save & Continue →</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<!-- Step 5: Paths -->
|
||||||
|
<!-- ═══════════════════════════════════════════════════════════════ -->
|
||||||
|
<div id="step-5" class="card" style="display:none">
|
||||||
|
<div class="card-title">Web Root & 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">
|
||||||
|
• /opt/seteclabs/docker-compose.yml<br>
|
||||||
|
• /root/docker-compose.yml<br>
|
||||||
|
• /srv/docker-compose.yml
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top:10px">
|
||||||
|
<button class="btn" onclick="goStep(4)">← Back</button>
|
||||||
|
<button class="btn" onclick="savePaths()">Save & Finish Setup →</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> → click your profile icon → <strong>API Keys</strong> → Create new key with DNS permissions.',
|
||||||
|
cloudflare: 'Go to <strong>dash.cloudflare.com</strong> → My Profile → <strong>API Tokens</strong> → Create Token → use "Edit zone DNS" template.',
|
||||||
|
digitalocean: 'Go to <strong>cloud.digitalocean.com</strong> → API → <strong>Tokens</strong> → Generate New Token with read+write scope.',
|
||||||
|
vultr: 'Go to <strong>my.vultr.com</strong> → Account → <strong>API</strong> → Enable API and copy the key.',
|
||||||
|
linode: 'Go to <strong>cloud.linode.com</strong> → My Profile → <strong>API Tokens</strong> → Create Personal Access Token.',
|
||||||
|
godaddy: 'Go to <strong>developer.godaddy.com</strong> → API Keys → Create New API Key. Format: <span style="color:#00ff41">key:secret</span>.',
|
||||||
|
namecheap: 'Go to <strong>namecheap.com</strong> → Profile → Tools → <strong>API Access</strong>. Whitelist your IP.',
|
||||||
|
hetzner: 'Go to <strong>dns.hetzner.com</strong> → API Tokens → 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> → VPS → Settings → <strong>SSH Keys</strong> → Add SSH Key.' },
|
||||||
|
digitalocean: { name: 'DigitalOcean', url: 'https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/', steps: 'Go to <strong>Settings</strong> → <strong>Security</strong> → SSH Keys → Add SSH Key.' },
|
||||||
|
vultr: { name: 'Vultr', url: 'https://docs.vultr.com/how-do-i-generate-ssh-keys', steps: 'Go to <strong>Account</strong> → <strong>SSH Keys</strong> → 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> → <strong>SSH Keys</strong> → 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> → <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 → <strong>Public Cloud</strong> → <strong>SSH Keys</strong>.' },
|
||||||
|
aws: { name: 'AWS (EC2)', url: 'https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html', steps: 'AWS Console → EC2 → <strong>Key Pairs</strong> → 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 & 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
53
setec-web/wsgi.py
Normal 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()
|
||||||
55
updates/sec/debian12_0326.sec
Normal file
55
updates/sec/debian12_0326.sec
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Setec Labs Security Update — Debian 12 Bookworm
|
||||||
|
# Release: 2026-03
|
||||||
|
# Target: Debian 12 (Bookworm)
|
||||||
|
# Author: seteclabs.io
|
||||||
|
# Applied via: sec_updates.py parse_and_apply_cmd()
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
# Critical security patches for March 2026
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y --only-upgrade openssl libssl3
|
||||||
|
apt-get install -y --only-upgrade libgnutls30
|
||||||
|
apt-get install -y --only-upgrade openssh-server openssh-client
|
||||||
|
apt-get install -y --only-upgrade curl libcurl4
|
||||||
|
apt-get install -y --only-upgrade sudo
|
||||||
|
apt-get install -y --only-upgrade systemd libsystemd0
|
||||||
|
apt-get install -y --only-upgrade linux-image-amd64
|
||||||
|
apt-get install -y --only-upgrade bind9-dnsutils bind9-host
|
||||||
|
apt-get install -y --only-upgrade nginx
|
||||||
|
apt-get install -y --only-upgrade python3 libpython3-stdlib
|
||||||
|
apt-get install -y --only-upgrade git
|
||||||
|
apt-get install -y --only-upgrade vim-common xxd
|
||||||
|
|
||||||
|
[sysctl]
|
||||||
|
# Harden network stack
|
||||||
|
net.ipv4.tcp_syncookies = 1
|
||||||
|
net.ipv4.conf.all.rp_filter = 1
|
||||||
|
net.ipv4.conf.default.rp_filter = 1
|
||||||
|
net.ipv4.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.default.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.send_redirects = 0
|
||||||
|
net.ipv6.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.accept_source_route = 0
|
||||||
|
net.ipv4.icmp_echo_ignore_broadcasts = 1
|
||||||
|
kernel.randomize_va_space = 2
|
||||||
|
fs.protected_hardlinks = 1
|
||||||
|
fs.protected_symlinks = 1
|
||||||
|
kernel.kptr_restrict = 1
|
||||||
|
kernel.dmesg_restrict = 1
|
||||||
|
|
||||||
|
[services]
|
||||||
|
systemctl restart ssh
|
||||||
|
systemctl restart nginx
|
||||||
|
systemctl daemon-reexec
|
||||||
|
|
||||||
|
[files]
|
||||||
|
chmod 600 /etc/ssh/sshd_config
|
||||||
|
chmod 700 /root/.ssh
|
||||||
|
chmod 644 /etc/sysctl.d/99-setec-update.conf
|
||||||
|
chown root:root /etc/sysctl.d/99-setec-update.conf
|
||||||
|
|
||||||
|
[custom]
|
||||||
|
# Cleanup old kernels
|
||||||
|
bash:-:apt-get autoremove -y --purge 2>&1
|
||||||
|
# Verify no known vulnerable packages remain
|
||||||
|
bash:-:apt-get -s upgrade 2>&1 | grep -c 'upgraded' || echo '0 remaining upgrades'
|
||||||
53
updates/sec/debian13_0326.sec
Normal file
53
updates/sec/debian13_0326.sec
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# Setec Labs Security Update — Debian 13 Trixie
|
||||||
|
# Release: 2026-03
|
||||||
|
# Target: Debian 13 (Trixie)
|
||||||
|
# Author: seteclabs.io
|
||||||
|
# Note: Trixie is the current testing/stable release
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y --only-upgrade openssl libssl3t64
|
||||||
|
apt-get install -y --only-upgrade libgnutls30t64
|
||||||
|
apt-get install -y --only-upgrade openssh-server openssh-client
|
||||||
|
apt-get install -y --only-upgrade curl libcurl4t64
|
||||||
|
apt-get install -y --only-upgrade sudo
|
||||||
|
apt-get install -y --only-upgrade systemd libsystemd0
|
||||||
|
apt-get install -y --only-upgrade linux-image-amd64
|
||||||
|
apt-get install -y --only-upgrade nginx
|
||||||
|
apt-get install -y --only-upgrade python3 python3-minimal
|
||||||
|
apt-get install -y --only-upgrade git
|
||||||
|
apt-get install -y --only-upgrade vim-common xxd
|
||||||
|
apt-get install -y --only-upgrade libc6
|
||||||
|
|
||||||
|
[sysctl]
|
||||||
|
net.ipv4.tcp_syncookies = 1
|
||||||
|
net.ipv4.conf.all.rp_filter = 1
|
||||||
|
net.ipv4.conf.default.rp_filter = 1
|
||||||
|
net.ipv4.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.default.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.send_redirects = 0
|
||||||
|
net.ipv6.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.accept_source_route = 0
|
||||||
|
net.ipv4.icmp_echo_ignore_broadcasts = 1
|
||||||
|
kernel.randomize_va_space = 2
|
||||||
|
fs.protected_hardlinks = 1
|
||||||
|
fs.protected_symlinks = 1
|
||||||
|
kernel.kptr_restrict = 1
|
||||||
|
kernel.dmesg_restrict = 1
|
||||||
|
# Trixie supports unprivileged userns restrictions
|
||||||
|
kernel.unprivileged_userns_clone = 0
|
||||||
|
|
||||||
|
[services]
|
||||||
|
systemctl restart ssh
|
||||||
|
systemctl restart nginx
|
||||||
|
systemctl daemon-reexec
|
||||||
|
|
||||||
|
[files]
|
||||||
|
chmod 600 /etc/ssh/sshd_config
|
||||||
|
chmod 700 /root/.ssh
|
||||||
|
chmod 644 /etc/sysctl.d/99-setec-update.conf
|
||||||
|
chown root:root /etc/sysctl.d/99-setec-update.conf
|
||||||
|
|
||||||
|
[custom]
|
||||||
|
bash:-:apt-get autoremove -y --purge 2>&1
|
||||||
|
bash:-:apt-get -s upgrade 2>&1 | grep -c 'upgraded' || echo '0 remaining upgrades'
|
||||||
55
updates/sec/ubuntu2204_0326.sec
Normal file
55
updates/sec/ubuntu2204_0326.sec
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Setec Labs Security Update — Ubuntu 22.04 LTS Jammy
|
||||||
|
# Release: 2026-03
|
||||||
|
# Target: Ubuntu 22.04 (Jammy Jellyfish)
|
||||||
|
# Author: seteclabs.io
|
||||||
|
# EOL: April 2027 (standard), April 2032 (ESM)
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y --only-upgrade openssl libssl3
|
||||||
|
apt-get install -y --only-upgrade libgnutls30
|
||||||
|
apt-get install -y --only-upgrade openssh-server openssh-client
|
||||||
|
apt-get install -y --only-upgrade curl libcurl4
|
||||||
|
apt-get install -y --only-upgrade sudo
|
||||||
|
apt-get install -y --only-upgrade systemd libsystemd0
|
||||||
|
apt-get install -y --only-upgrade linux-image-generic
|
||||||
|
apt-get install -y --only-upgrade nginx
|
||||||
|
apt-get install -y --only-upgrade python3 python3-minimal
|
||||||
|
apt-get install -y --only-upgrade git
|
||||||
|
apt-get install -y --only-upgrade vim-common xxd
|
||||||
|
apt-get install -y --only-upgrade ca-certificates
|
||||||
|
apt-get install -y --only-upgrade apparmor
|
||||||
|
|
||||||
|
[sysctl]
|
||||||
|
net.ipv4.tcp_syncookies = 1
|
||||||
|
net.ipv4.conf.all.rp_filter = 1
|
||||||
|
net.ipv4.conf.default.rp_filter = 1
|
||||||
|
net.ipv4.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.default.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.send_redirects = 0
|
||||||
|
net.ipv6.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.accept_source_route = 0
|
||||||
|
net.ipv4.icmp_echo_ignore_broadcasts = 1
|
||||||
|
kernel.randomize_va_space = 2
|
||||||
|
fs.protected_hardlinks = 1
|
||||||
|
fs.protected_symlinks = 1
|
||||||
|
kernel.kptr_restrict = 1
|
||||||
|
kernel.dmesg_restrict = 1
|
||||||
|
|
||||||
|
[services]
|
||||||
|
systemctl restart ssh
|
||||||
|
systemctl restart nginx
|
||||||
|
systemctl restart apparmor
|
||||||
|
systemctl daemon-reexec
|
||||||
|
|
||||||
|
[files]
|
||||||
|
chmod 600 /etc/ssh/sshd_config
|
||||||
|
chmod 700 /root/.ssh
|
||||||
|
chmod 644 /etc/sysctl.d/99-setec-update.conf
|
||||||
|
chown root:root /etc/sysctl.d/99-setec-update.conf
|
||||||
|
|
||||||
|
[custom]
|
||||||
|
bash:-:apt-get autoremove -y --purge 2>&1
|
||||||
|
bash:-:apt-get -s upgrade 2>&1 | grep -c 'upgraded' || echo '0 remaining upgrades'
|
||||||
|
# Check Ubuntu Pro/ESM status
|
||||||
|
bash:-:pro status 2>/dev/null || echo 'Ubuntu Pro not configured'
|
||||||
49
updates/sec/ubuntu2210_0326.sec
Normal file
49
updates/sec/ubuntu2210_0326.sec
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Setec Labs Security Update — Ubuntu 22.10 Kinetic
|
||||||
|
# Release: 2026-03
|
||||||
|
# Target: Ubuntu 22.10 (Kinetic Kudu)
|
||||||
|
# Author: seteclabs.io
|
||||||
|
#
|
||||||
|
# !! WARNING: Ubuntu 22.10 reached End of Life on July 20, 2023 !!
|
||||||
|
# No further security patches are available from Canonical.
|
||||||
|
# This .sec file provides upgrade guidance only.
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
# IMPORTANT: 22.10 repos are archived. Update sources first if needed.
|
||||||
|
# sed -i 's|archive.ubuntu.com|old-releases.ubuntu.com|g' /etc/apt/sources.list
|
||||||
|
# sed -i 's|security.ubuntu.com|old-releases.ubuntu.com|g' /etc/apt/sources.list
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y --only-upgrade openssl libssl3
|
||||||
|
apt-get install -y --only-upgrade openssh-server openssh-client
|
||||||
|
apt-get install -y --only-upgrade curl libcurl4
|
||||||
|
apt-get install -y --only-upgrade sudo
|
||||||
|
|
||||||
|
[sysctl]
|
||||||
|
# Minimal hardening — upgrade ASAP
|
||||||
|
net.ipv4.tcp_syncookies = 1
|
||||||
|
net.ipv4.conf.all.rp_filter = 1
|
||||||
|
net.ipv4.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.send_redirects = 0
|
||||||
|
kernel.randomize_va_space = 2
|
||||||
|
fs.protected_hardlinks = 1
|
||||||
|
fs.protected_symlinks = 1
|
||||||
|
|
||||||
|
[services]
|
||||||
|
systemctl restart ssh
|
||||||
|
|
||||||
|
[files]
|
||||||
|
chmod 600 /etc/ssh/sshd_config
|
||||||
|
|
||||||
|
[custom]
|
||||||
|
# Display EOL warning prominently
|
||||||
|
bash:-:echo '=========================================='
|
||||||
|
bash:-:echo 'WARNING: Ubuntu 22.10 is END OF LIFE'
|
||||||
|
bash:-:echo 'No security patches since July 2023!'
|
||||||
|
bash:-:echo '=========================================='
|
||||||
|
bash:-:echo ''
|
||||||
|
bash:-:echo 'Recommended upgrade path:'
|
||||||
|
bash:-:echo ' 22.10 -> 23.04 -> 23.10 -> 24.04 LTS'
|
||||||
|
bash:-:echo ''
|
||||||
|
bash:-:echo 'Or fresh install Ubuntu 24.04 LTS (recommended)'
|
||||||
|
bash:-:echo ''
|
||||||
|
bash:-:echo 'To begin upgrade: do-release-upgrade'
|
||||||
|
bash:-:echo '=========================================='
|
||||||
57
updates/sec/ubuntu2404_0326.sec
Normal file
57
updates/sec/ubuntu2404_0326.sec
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Setec Labs Security Update — Ubuntu 24.04 LTS Noble
|
||||||
|
# Release: 2026-03
|
||||||
|
# Target: Ubuntu 24.04 (Noble Numbat)
|
||||||
|
# Author: seteclabs.io
|
||||||
|
# EOL: April 2029 (standard), April 2034 (ESM)
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y --only-upgrade openssl libssl3t64
|
||||||
|
apt-get install -y --only-upgrade libgnutls30t64
|
||||||
|
apt-get install -y --only-upgrade openssh-server openssh-client
|
||||||
|
apt-get install -y --only-upgrade curl libcurl4t64
|
||||||
|
apt-get install -y --only-upgrade sudo
|
||||||
|
apt-get install -y --only-upgrade systemd libsystemd0
|
||||||
|
apt-get install -y --only-upgrade linux-image-generic
|
||||||
|
apt-get install -y --only-upgrade nginx
|
||||||
|
apt-get install -y --only-upgrade python3 python3-minimal
|
||||||
|
apt-get install -y --only-upgrade git
|
||||||
|
apt-get install -y --only-upgrade vim-common xxd
|
||||||
|
apt-get install -y --only-upgrade ca-certificates
|
||||||
|
apt-get install -y --only-upgrade apparmor
|
||||||
|
apt-get install -y --only-upgrade libc6
|
||||||
|
|
||||||
|
[sysctl]
|
||||||
|
net.ipv4.tcp_syncookies = 1
|
||||||
|
net.ipv4.conf.all.rp_filter = 1
|
||||||
|
net.ipv4.conf.default.rp_filter = 1
|
||||||
|
net.ipv4.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.default.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.send_redirects = 0
|
||||||
|
net.ipv6.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.accept_source_route = 0
|
||||||
|
net.ipv4.icmp_echo_ignore_broadcasts = 1
|
||||||
|
kernel.randomize_va_space = 2
|
||||||
|
fs.protected_hardlinks = 1
|
||||||
|
fs.protected_symlinks = 1
|
||||||
|
kernel.kptr_restrict = 1
|
||||||
|
kernel.dmesg_restrict = 1
|
||||||
|
# Noble supports unprivileged userns restrictions
|
||||||
|
kernel.apparmor_restrict_unprivileged_userns = 1
|
||||||
|
|
||||||
|
[services]
|
||||||
|
systemctl restart ssh
|
||||||
|
systemctl restart nginx
|
||||||
|
systemctl restart apparmor
|
||||||
|
systemctl daemon-reexec
|
||||||
|
|
||||||
|
[files]
|
||||||
|
chmod 600 /etc/ssh/sshd_config
|
||||||
|
chmod 700 /root/.ssh
|
||||||
|
chmod 644 /etc/sysctl.d/99-setec-update.conf
|
||||||
|
chown root:root /etc/sysctl.d/99-setec-update.conf
|
||||||
|
|
||||||
|
[custom]
|
||||||
|
bash:-:apt-get autoremove -y --purge 2>&1
|
||||||
|
bash:-:apt-get -s upgrade 2>&1 | grep -c 'upgraded' || echo '0 remaining upgrades'
|
||||||
|
bash:-:pro status 2>/dev/null || echo 'Ubuntu Pro not configured'
|
||||||
57
updates/sec/ubuntu2410_0326.sec
Normal file
57
updates/sec/ubuntu2410_0326.sec
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Setec Labs Security Update — Ubuntu 24.10 Oracular
|
||||||
|
# Release: 2026-03
|
||||||
|
# Target: Ubuntu 24.10 (Oracular Oriole)
|
||||||
|
# Author: seteclabs.io
|
||||||
|
# EOL: July 2025 — approaching end of life, plan upgrade to 25.04 or 24.04 LTS
|
||||||
|
|
||||||
|
[packages]
|
||||||
|
apt-get update -qq
|
||||||
|
apt-get install -y --only-upgrade openssl libssl3t64
|
||||||
|
apt-get install -y --only-upgrade libgnutls30t64
|
||||||
|
apt-get install -y --only-upgrade openssh-server openssh-client
|
||||||
|
apt-get install -y --only-upgrade curl libcurl4t64
|
||||||
|
apt-get install -y --only-upgrade sudo
|
||||||
|
apt-get install -y --only-upgrade systemd libsystemd0
|
||||||
|
apt-get install -y --only-upgrade linux-image-generic
|
||||||
|
apt-get install -y --only-upgrade nginx
|
||||||
|
apt-get install -y --only-upgrade python3 python3-minimal
|
||||||
|
apt-get install -y --only-upgrade git
|
||||||
|
apt-get install -y --only-upgrade vim-common xxd
|
||||||
|
apt-get install -y --only-upgrade ca-certificates
|
||||||
|
apt-get install -y --only-upgrade apparmor
|
||||||
|
apt-get install -y --only-upgrade libc6
|
||||||
|
|
||||||
|
[sysctl]
|
||||||
|
net.ipv4.tcp_syncookies = 1
|
||||||
|
net.ipv4.conf.all.rp_filter = 1
|
||||||
|
net.ipv4.conf.default.rp_filter = 1
|
||||||
|
net.ipv4.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.default.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.send_redirects = 0
|
||||||
|
net.ipv6.conf.all.accept_redirects = 0
|
||||||
|
net.ipv4.conf.all.accept_source_route = 0
|
||||||
|
net.ipv4.icmp_echo_ignore_broadcasts = 1
|
||||||
|
kernel.randomize_va_space = 2
|
||||||
|
fs.protected_hardlinks = 1
|
||||||
|
fs.protected_symlinks = 1
|
||||||
|
kernel.kptr_restrict = 1
|
||||||
|
kernel.dmesg_restrict = 1
|
||||||
|
kernel.apparmor_restrict_unprivileged_userns = 1
|
||||||
|
|
||||||
|
[services]
|
||||||
|
systemctl restart ssh
|
||||||
|
systemctl restart nginx
|
||||||
|
systemctl restart apparmor
|
||||||
|
systemctl daemon-reexec
|
||||||
|
|
||||||
|
[files]
|
||||||
|
chmod 600 /etc/ssh/sshd_config
|
||||||
|
chmod 700 /root/.ssh
|
||||||
|
chmod 644 /etc/sysctl.d/99-setec-update.conf
|
||||||
|
chown root:root /etc/sysctl.d/99-setec-update.conf
|
||||||
|
|
||||||
|
[custom]
|
||||||
|
bash:-:apt-get autoremove -y --purge 2>&1
|
||||||
|
bash:-:apt-get -s upgrade 2>&1 | grep -c 'upgraded' || echo '0 remaining upgrades'
|
||||||
|
# Reminder: 24.10 is a short-term release
|
||||||
|
bash:-:echo 'NOTE: Ubuntu 24.10 support ends July 2025. Consider upgrading to 25.04 or 24.04 LTS.'
|
||||||
Reference in New Issue
Block a user