import type { Client } from 'ssh2' import { execCommand } from './common.js' export interface ListeningPort { protocol: 'tcp' | 'udp' localAddress: string localPort: number state?: string pid?: number process?: string } export interface PortsMetrics { source: 'ss' | 'netstat' | 'none' ports: ListeningPort[] } function parseSsOutput(output: string): ListeningPort[] { const ports: ListeningPort[] = [] const lines = output.split('\n').slice(1) for (const line of lines) { const parts = line.trim().split(/\s+/) if (parts.length < 5) continue const protocol = parts[0]?.toLowerCase() if (protocol !== 'tcp' && protocol !== 'udp') continue const state = parts[1] const localAddr = parts[4] if (!localAddr) continue const lastColon = localAddr.lastIndexOf(':') if (lastColon === -1) continue const address = localAddr.substring(0, lastColon).replace(/^\[|\]$/g, '') const port = parseInt(localAddr.substring(lastColon + 1), 10) if (isNaN(port)) continue const entry: ListeningPort = { protocol, localAddress: address, localPort: port, state: protocol === 'tcp' ? state : undefined, } const processInfo = parts[6] if (processInfo?.startsWith('users:')) { const pidMatch = processInfo.match(/pid=(\d+)/) const nameMatch = processInfo.match(/\("([^"]+)"/) if (pidMatch) entry.pid = parseInt(pidMatch[1], 10) if (nameMatch) entry.process = nameMatch[1] } ports.push(entry) } return ports } export async function collectPortsMetrics(client: Client): Promise { try { const ssResult = await execCommand(client, 'ss -tulnp 2>/dev/null') if (ssResult.stdout.includes('Local')) { return { source: 'ss', ports: parseSsOutput(ssResult.stdout).sort((a, b) => a.localPort - b.localPort) } } return { source: 'none', ports: [] } } catch { return { source: 'none', ports: [] } } }