MEDIUM-SEVERITY FIXES (Fase 3 complete): 1. Mock Data em Produção (Vulnerabilidade 3.2) ✅ - Mock data apenas em desenvolvimento (import.meta.env.DEV) - Produção mostra erro claro com retry button - Estado de erro com UI profissional 2. Connection Pool Timeouts (Vulnerabilidade 3.3) ✅ - JÁ CORRIGIDO em commit anterior (20c16ab) - connectTimeout: 10s, acquireTimeout: 15s, timeout: 30s 3. Tipo 'any' em Catch Blocks (Vulnerabilidade 3.4) ✅ - TODOS os ficheiros corrigidos (10/10) - catch (error: unknown) em vez de catch (error) - Type guards: error instanceof Error - Mensagens seguras sem vazamento de stack trace - Ficheiros: routes/*.ts, services/*.ts, middleware/validation.ts 4. APIs Sem Autenticação Backend (Vulnerabilidade 3.5) ✅ - JÁ IMPLEMENTADO em commit anterior (f175682) - OIDC opcional via OIDC_ENABLED=true 5. Algoritmos SSH Legacy (Vulnerabilidade 3.6) ✅ - Adicionados: curve25519-sha256, curve25519-sha256@libssh.org - Removidos: diffie-hellman-group14-sha1 (legacy) - Removidos: diffie-hellman-group1-sha1 (INSEGURO) - Apenas SHA256+ algorithms mantidos 6. Configuração OIDC (Vulnerabilidade 3.1) ✅ - JÁ IMPLEMENTADO em commit anterior (f175682) - OIDC completamente funcional (opcional) FILES CHANGED: - src/App.tsx - Error state + mock data apenas em dev - api/routes/*.ts - Tipos unknown em todos os catch blocks - api/services/*.ts - Tipos unknown em todos os catch blocks - api/middleware/validation.ts - Tipo correto (error.issues) - api/services/server-metrics.ts - Algoritmos SSH modernos BUILD STATUS: - TypeScript: ✅ PASSED - npm run build: ✅ SUCCESS - npm audit: ✅ 0 vulnerabilities PROGRESS: - Phase 1 (Critical): 3/3 ✅ COMPLETE - Phase 2 (High): 6/6 ✅ COMPLETE - Phase 3 (Medium): 6/6 ✅ COMPLETE - Phase 4 (Low): 0/5 - Next Related: AUDIT-REPORT.md vulnerabilities 3.1, 3.2, 3.3, 3.4, 3.5, 3.6 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
246 lines
6.9 KiB
TypeScript
Executable File
246 lines
6.9 KiB
TypeScript
Executable File
/**
|
|
* Server Metrics Collector Service
|
|
* Recolhe métricas de todos os servidores via SSH (ssh2 library)
|
|
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
|
|
*/
|
|
import db from '../db.js'
|
|
import { Client } from 'ssh2'
|
|
|
|
interface SSHServer {
|
|
name: string
|
|
monitorName: string
|
|
host: string
|
|
port: number
|
|
user: string
|
|
pass: string
|
|
}
|
|
|
|
const SSH_SERVERS: SSHServer[] = [
|
|
{
|
|
name: 'server',
|
|
monitorName: 'CWP Server',
|
|
host: process.env.SERVER_HOST || '176.9.3.158',
|
|
port: parseInt(process.env.SERVER_PORT || '9443'),
|
|
user: process.env.SERVER_USER || 'root',
|
|
pass: process.env.SERVER_PASS || ''
|
|
},
|
|
{
|
|
name: 'easy',
|
|
monitorName: 'EasyPanel',
|
|
host: process.env.EASY_HOST || '178.63.18.51',
|
|
port: 22,
|
|
user: process.env.EASY_USER || 'root',
|
|
pass: process.env.EASY_PASS || ''
|
|
},
|
|
{
|
|
name: 'mcp-hub',
|
|
monitorName: 'MCP Hub',
|
|
host: process.env.MCPHUB_HOST || 'mcp-hub.descomplicar.pt',
|
|
port: 22,
|
|
user: process.env.MCPHUB_USER || 'root',
|
|
pass: process.env.MCPHUB_PASS || ''
|
|
},
|
|
{
|
|
name: 'meet',
|
|
monitorName: 'Meet',
|
|
host: process.env.MEET_HOST || 'meet.descomplicar.pt',
|
|
port: 22,
|
|
user: process.env.MEET_USER || 'root',
|
|
pass: process.env.MEET_PASS || ''
|
|
},
|
|
{
|
|
name: 'whatsapp',
|
|
monitorName: 'WhatsApp',
|
|
host: process.env.WHATSAPP_HOST || 'whatsapp.descomplicar.pt',
|
|
port: 22,
|
|
user: process.env.WHATSAPP_USER || 'root',
|
|
pass: process.env.WHATSAPP_PASS || ''
|
|
},
|
|
{
|
|
name: 'whatsms',
|
|
monitorName: 'WhatSMS',
|
|
host: process.env.WHATSMS_HOST || 'whatsms.descomplicar.pt',
|
|
port: 22,
|
|
user: process.env.WHATSMS_USER || 'root',
|
|
pass: process.env.WHATSMS_PASS || ''
|
|
}
|
|
]
|
|
|
|
interface ServerMetrics {
|
|
cpu: number
|
|
ram: number
|
|
disk: number
|
|
load: number
|
|
containers?: number
|
|
}
|
|
|
|
function parseSSHMetrics(output: string): ServerMetrics {
|
|
const lines = output.split('\n')
|
|
const metrics: ServerMetrics = { cpu: 0, ram: 0, disk: 0, load: 0 }
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('CPU:')) metrics.cpu = parseFloat(line.split(':')[1]) || 0
|
|
if (line.startsWith('MEM:')) metrics.ram = parseFloat(line.split(':')[1]) || 0
|
|
if (line.startsWith('DISK:')) metrics.disk = parseFloat(line.split(':')[1]) || 0
|
|
if (line.startsWith('LOAD:')) metrics.load = parseFloat(line.split(':')[1]) || 0
|
|
if (line.startsWith('CONTAINERS:')) metrics.containers = parseInt(line.split(':')[1]) || 0
|
|
}
|
|
|
|
return metrics
|
|
}
|
|
|
|
/**
|
|
* Execute SSH command via ssh2 library
|
|
*/
|
|
function executeSSH(server: SSHServer, command: string): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
const conn = new Client()
|
|
let output = ''
|
|
const timeout = setTimeout(() => {
|
|
conn.end()
|
|
reject(new Error(`SSH timeout for ${server.name}`))
|
|
}, 20000)
|
|
|
|
conn.on('ready', () => {
|
|
conn.exec(command, (err, stream) => {
|
|
if (err) {
|
|
clearTimeout(timeout)
|
|
conn.end()
|
|
reject(err)
|
|
return
|
|
}
|
|
stream.on('data', (data: Buffer) => {
|
|
output += data.toString()
|
|
})
|
|
stream.on('close', () => {
|
|
clearTimeout(timeout)
|
|
conn.end()
|
|
resolve(output)
|
|
})
|
|
stream.stderr.on('data', () => {
|
|
// ignore stderr
|
|
})
|
|
})
|
|
})
|
|
|
|
conn.on('error', (err) => {
|
|
clearTimeout(timeout)
|
|
reject(err)
|
|
})
|
|
|
|
conn.connect({
|
|
host: server.host,
|
|
port: server.port,
|
|
username: server.user,
|
|
password: server.pass,
|
|
readyTimeout: 15000,
|
|
algorithms: {
|
|
kex: [
|
|
// Algoritmos modernos (Vulnerabilidade 3.6)
|
|
'curve25519-sha256',
|
|
'curve25519-sha256@libssh.org',
|
|
'ecdh-sha2-nistp256',
|
|
'ecdh-sha2-nistp384',
|
|
'ecdh-sha2-nistp521',
|
|
'diffie-hellman-group-exchange-sha256',
|
|
'diffie-hellman-group14-sha256'
|
|
// REMOVIDOS (inseguros): diffie-hellman-group14-sha1, diffie-hellman-group1-sha1
|
|
]
|
|
}
|
|
})
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Collect metrics from all SSH servers
|
|
*/
|
|
export async function collectSSHMetrics(): Promise<{ success: number; failed: number }> {
|
|
let success = 0
|
|
let failed = 0
|
|
|
|
const metricsCommand = `echo "CPU:$(top -bn1 | grep 'Cpu(s)' | awk '{print $2}')"; echo "MEM:$(free -m | awk '/Mem:/ {printf "%.1f", $3/$2*100}')"; echo "DISK:$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')"; echo "LOAD:$(cat /proc/loadavg | awk '{print $1}')"; echo "CONTAINERS:$(docker ps -q 2>/dev/null | wc -l || echo 0)"`
|
|
|
|
for (const server of SSH_SERVERS) {
|
|
if (!server.pass) {
|
|
console.log(`[SSH] Skipping ${server.name}: no password configured`)
|
|
failed++
|
|
continue
|
|
}
|
|
|
|
try {
|
|
const output = await executeSSH(server, metricsCommand)
|
|
if (!output) {
|
|
failed++
|
|
continue
|
|
}
|
|
|
|
const metrics = parseSSHMetrics(output)
|
|
|
|
// Update monitoring table
|
|
await db.query(
|
|
`UPDATE tbl_eal_monitoring
|
|
SET details = ?, status = 'up', last_check = NOW()
|
|
WHERE category = 'server' AND name = ?`,
|
|
[
|
|
JSON.stringify({
|
|
cpu: metrics.cpu,
|
|
ram: metrics.ram,
|
|
disk: metrics.disk,
|
|
load: metrics.load
|
|
}),
|
|
server.monitorName
|
|
]
|
|
)
|
|
|
|
// Update containers if EasyPanel
|
|
if (server.name === 'easy' && metrics.containers !== undefined) {
|
|
try {
|
|
const containerOutput = await executeSSH(server, 'docker ps -a --format "{{.Status}}" | grep -c "Up" || echo 0; docker ps -aq | wc -l')
|
|
const [up, total] = containerOutput.trim().split('\n').map(n => parseInt(n) || 0)
|
|
const down = total - up
|
|
|
|
await db.query(
|
|
`UPDATE tbl_eal_monitoring
|
|
SET details = ?, status = ?, last_check = NOW()
|
|
WHERE category = 'container' AND name = 'EasyPanel Containers'`,
|
|
[
|
|
JSON.stringify({ total, up, down, restarting: 0 }),
|
|
down > 0 ? 'warning' : 'ok'
|
|
]
|
|
)
|
|
} catch {
|
|
// Container stats are optional
|
|
}
|
|
}
|
|
|
|
success++
|
|
console.log(`[SSH] ${server.monitorName}: CPU=${metrics.cpu}%, RAM=${metrics.ram}%, Disk=${metrics.disk}%`)
|
|
|
|
} catch (error: unknown) {
|
|
console.error(`[SSH] Failed ${server.name}:`, error instanceof Error ? error.message : 'Unknown error')
|
|
failed++
|
|
}
|
|
}
|
|
|
|
return { success, failed }
|
|
}
|
|
|
|
/**
|
|
* Collect all server metrics (SSH only - replaces Hetzner sync)
|
|
*/
|
|
export async function collectAllServerMetrics(): Promise<{
|
|
ssh: { success: number; failed: number }
|
|
}> {
|
|
console.log('[METRICS] Collecting server metrics via SSH...')
|
|
const ssh = await collectSSHMetrics()
|
|
console.log(`[METRICS] Done: ${ssh.success} OK, ${ssh.failed} failed`)
|
|
return { ssh }
|
|
}
|
|
|
|
// Keep for backward compatibility with routes
|
|
export async function syncHetznerToMonitoring(): Promise<number> {
|
|
// Now handled by SSH collection
|
|
const result = await collectSSHMetrics()
|
|
return result.success
|
|
}
|