telex-server-monitor-sdk
Version:
SDK for Telex Server Monitor - A lightweight agent that collects and reports server CPU metrics to the Telex monitoring platform
456 lines (455 loc) • 17.8 kB
JavaScript
import { exec } from "child_process";
import { promisify } from "util";
import fs from "fs";
import { logger } from "../utils/logger.js";
const execAsync = promisify(exec);
/**
* Get security metrics including:
* - Failed login attempts
* - SSH access logs
* - Firewall statistics
* - Port scanning detection
*/
export async function getSecurityMetrics() {
try {
// Run these operations in parallel for efficiency
const [failedLoginData, sshAccessData, firewallStats, portScanningData] = await Promise.all([
getFailedLogins(),
getSSHAccessLogs(),
getFirewallStatistics(),
getPortScanningDetection(),
]);
return {
security: {
failedLogins: failedLoginData,
sshAccess: sshAccessData,
firewall: firewallStats,
portScanning: portScanningData,
lastUpdated: new Date().toISOString(),
},
};
}
catch (error) {
logger.error(`Failed to get security metrics: ${error.message}`);
return {
security: {
failedLogins: { count: 0, recent: [] },
sshAccess: { count: 0, recent: [] },
firewall: { connections: 0, blocked: 0, rules: [] },
portScanning: { detected: false, attempts: [] },
lastUpdated: new Date().toISOString(),
error: error.message,
},
};
}
}
/**
* Get failed login attempts from auth.log
*/
async function getFailedLogins() {
try {
// Try several common locations for auth logs
const possibleLogPaths = [
"/var/log/auth.log", // Debian/Ubuntu
"/var/log/secure", // RHEL/CentOS
"/var/log/system.log", // macOS
];
let logContent = "";
let logPath = "";
// Find the first available log file
for (const path of possibleLogPaths) {
if (fs.existsSync(path)) {
logPath = path;
// Read the last 500 lines of the log file to limit memory usage
const { stdout } = await execAsync(`tail -n 500 ${path}`);
logContent = stdout;
break;
}
}
if (!logContent) {
// If no standard log files found, try using the journalctl command
try {
const { stdout } = await execAsync("journalctl -u sshd -n 100 --no-pager");
logContent = stdout;
}
catch (e) {
// If journalctl fails, return empty results
return { count: 0, recent: [] };
}
}
// Look for failed login patterns across different systems
const failedLoginPatterns = [
/Failed password for (.*?) from (.*?) port/, // SSH failed password
/Failed password for invalid user (.*?) from (.*?) port/, // SSH failed password for invalid user
/authentication failure.*?user=(.*?) rhost=(.*?)/, // PAM authentication failure
/FAILED LOGIN/i, // General failed login
];
const failedLogins = [];
const lines = logContent.split("\n");
// Process each line in the log content
for (const line of lines) {
if (line.trim()) {
for (const pattern of failedLoginPatterns) {
if (pattern.test(line)) {
const matches = line.match(pattern);
const timestamp = extractTimestamp(line);
let user = "unknown";
let source = "unknown";
if (matches && matches.length > 1) {
user = matches[1];
if (matches.length > 2) {
source = matches[2];
}
}
failedLogins.push({
timestamp,
user,
source,
message: line.substring(0, 150), // Limit message length
});
break;
}
}
}
}
// Sort most recent first and limit to 10
const recentFailedLogins = failedLogins
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, 10);
return {
count: failedLogins.length,
recent: recentFailedLogins,
};
}
catch (error) {
logger.error(`Failed to get failed logins: ${error.message}`);
return { count: 0, recent: [] };
}
}
/**
* Get SSH access logs
*/
async function getSSHAccessLogs() {
try {
let logContent = "";
// Try to get SSH logs using different methods
try {
// Try to use the journalctl command first (modern systems)
const { stdout } = await execAsync("journalctl -u sshd -n 100 --no-pager");
logContent = stdout;
}
catch (e) {
// Fall back to log files if journalctl fails
const possibleLogPaths = [
"/var/log/auth.log",
"/var/log/secure",
"/var/log/system.log",
];
for (const path of possibleLogPaths) {
if (fs.existsSync(path)) {
const { stdout } = await execAsync(`grep "sshd" ${path} | tail -n 100`);
logContent = stdout;
break;
}
}
}
if (!logContent) {
return { count: 0, recent: [] };
}
// Look for successful SSH login patterns
const sshAccessPattern = /Accepted (password|publickey) for (.*?) from (.*?) port/;
const sshAccesses = [];
const lines = logContent.split("\n");
for (const line of lines) {
if (line.trim() && sshAccessPattern.test(line)) {
const matches = line.match(sshAccessPattern);
const timestamp = extractTimestamp(line);
let authMethod = "unknown";
let user = "unknown";
let source = "unknown";
if (matches && matches.length > 1) {
authMethod = matches[1];
if (matches.length > 2) {
user = matches[2];
}
if (matches.length > 3) {
source = matches[3];
}
}
sshAccesses.push({
timestamp,
user,
source,
message: `Accepted ${authMethod} for ${user} from ${source}`,
});
}
}
// Sort most recent first and limit to 10
const recentAccesses = sshAccesses
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, 10);
return {
count: sshAccesses.length,
recent: recentAccesses,
};
}
catch (error) {
logger.error(`Failed to get SSH access logs: ${error.message}`);
return { count: 0, recent: [] };
}
}
/**
* Get firewall statistics
*/
async function getFirewallStatistics() {
try {
let connections = 0;
let blocked = 0;
const rules = [];
// Try to get firewall stats using iptables (Linux)
try {
// Get existing connections
const { stdout: connectionsOutput } = await execAsync("iptables -L -n -v | grep -v '0 0'");
const connectionLines = connectionsOutput
.split("\n")
.filter((line) => line.trim());
connections = connectionLines.length;
// Count blocked connections (typically in DROP or REJECT targets)
blocked = connectionLines.filter((line) => line.includes("DROP") || line.includes("REJECT")).length;
// Get rules summary
const { stdout: rulesOutput } = await execAsync("iptables -L -n | head -n 30");
const ruleLines = rulesOutput.split("\n").filter((line) => line.trim());
let currentChain = "";
for (const line of ruleLines) {
if (line.startsWith("Chain")) {
currentChain = line.split(" ")[1];
}
else if (line.startsWith("target")) {
// Skip headers
continue;
}
else if (line.trim()) {
const parts = line.split(/\s+/);
if (parts.length >= 4) {
rules.push({
chain: currentChain,
target: parts[0],
protocol: parts[1],
source: parts[3],
destination: parts[4] || "any",
});
}
}
}
}
catch (e) {
// Try to use pfctl on macOS/BSD
try {
const { stdout: pfInfo } = await execAsync("pfctl -s info");
const blockedMatch = pfInfo.match(/Blocked packets: (\d+)/);
if (blockedMatch) {
blocked = parseInt(blockedMatch[1], 10);
}
const statesMatch = pfInfo.match(/Current states: (\d+)/);
if (statesMatch) {
connections = parseInt(statesMatch[1], 10);
}
// Get a sample of rules
const { stdout: pfRules } = await execAsync("pfctl -s rules | head -n 10");
const ruleLines = pfRules.split("\n").filter((line) => line.trim());
for (const line of ruleLines) {
if (line.includes("block") || line.includes("pass")) {
const action = line.includes("block") ? "block" : "pass";
const proto = line.includes("proto ")
? line.split("proto ")[1].split(" ")[0]
: "any";
rules.push({
chain: "filter",
target: action,
protocol: proto,
source: line.includes("from ")
? line.split("from ")[1].split(" ")[0]
: "any",
destination: line.includes("to ")
? line.split("to ")[1].split(" ")[0]
: "any",
});
}
}
}
catch (pfError) {
// If both methods fail, leave defaults (0 connections, 0 blocked, empty rules)
}
}
return {
connections,
blocked,
rules: rules.slice(0, 5), // Limit to 5 rules for readability
};
}
catch (error) {
logger.error(`Failed to get firewall statistics: ${error.message}`);
return {
connections: 0,
blocked: 0,
rules: [],
};
}
}
/**
* Detect potential port scanning activity
*/
async function getPortScanningDetection() {
try {
let logContent = "";
// Try to check logs for port scanning signs
const possibleLogPaths = [
"/var/log/auth.log",
"/var/log/secure",
"/var/log/firewall.log",
"/var/log/ufw.log",
];
for (const path of possibleLogPaths) {
if (fs.existsSync(path)) {
const { stdout } = await execAsync(`grep -i "scan\|multiple\|port" ${path} | tail -n 100`);
logContent += stdout + "\n";
}
}
// Also try to use journalctl for firewall logs
try {
const { stdout: journalLogs } = await execAsync("journalctl -k -g 'port.*scan' -n 50 --no-pager");
logContent += journalLogs;
}
catch (e) {
// Ignore errors from journalctl
}
// If no logs found, try using the netstat command to detect many connections from single sources
if (!logContent) {
const { stdout: netstatOutput } = await execAsync("netstat -tn | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr | head -n 10");
// Process netstat output to find potential scanners
const lines = netstatOutput.split("\n");
const potentialScanners = lines
.map((line) => {
const parts = line.trim().split(/\s+/);
if (parts.length >= 2) {
const count = parseInt(parts[0], 10);
const ip = parts[1];
// If a single IP has many connections, it might be scanning
if (count > 10 && ip !== "*" && !ip.startsWith("127.")) {
return {
timestamp: new Date().toISOString(),
source: ip,
ports: "multiple",
message: `${ip} has ${count} concurrent connections, possible port scanning`,
};
}
}
return null;
})
.filter((item) => item !== null);
return {
detected: potentialScanners.length > 0,
attempts: potentialScanners,
};
}
// Process log content for port scanning signs
const scanPatterns = [
/port scan/i,
/multiple ports/i,
/PSH flags/i,
/multiple connections/i,
/scan detected/i,
];
const scanAttempts = [];
const lines = logContent.split("\n");
for (const line of lines) {
if (line.trim()) {
for (const pattern of scanPatterns) {
if (pattern.test(line)) {
const timestamp = extractTimestamp(line);
// Try to extract IP addresses from the line
const ipMatch = line.match(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/);
const source = ipMatch ? ipMatch[0] : "unknown";
// Try to extract port information
const portMatch = line.match(/port (\d+)/i) ||
line.match(/ports? \s?(\d+[-,\d\s]*)/i);
const ports = portMatch ? portMatch[1] : "unknown";
scanAttempts.push({
timestamp,
source,
ports,
message: line.substring(0, 150), // Limit message length
});
break;
}
}
}
}
// Sort most recent first and limit to 10
const recentAttempts = scanAttempts
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
.slice(0, 10);
return {
detected: recentAttempts.length > 0,
attempts: recentAttempts,
};
}
catch (error) {
logger.error(`Failed to detect port scanning: ${error.message}`);
return {
detected: false,
attempts: [],
};
}
}
/**
* Helper function to extract timestamp from log lines
*/
function extractTimestamp(line) {
try {
// Try to match common timestamp patterns in logs
const timestampPatterns = [
/(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})/, // Aug 21 07:42:26
/(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+[+-]\d{4})/, // ISO format
/(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})/, // 2023-08-21 07:42:26
/(\w{3}\s+\d{1,2},?\s+\d{4}\s+\d{2}:\d{2}:\d{2})/, // Aug 21, 2023 07:42:26
];
for (const pattern of timestampPatterns) {
const match = line.match(pattern);
if (match && match[1]) {
return match[1];
}
}
// If no timestamp found, return current time
return new Date().toISOString();
}
catch (error) {
return new Date().toISOString();
}
}
/**
* Check for security threshold breaches
* @param securityMetrics Security metrics data
* @param failedLoginThreshold Maximum number of failed logins before alert (default: 5)
* @returns Alert status and severity
*/
export function checkSecurityThresholds(securityMetrics, failedLoginThreshold = 5) {
const alerts = [];
let isCritical = false;
// Check failed logins
if (securityMetrics.failedLogins &&
securityMetrics.failedLogins.count > failedLoginThreshold) {
alerts.push(`Excessive failed logins: ${securityMetrics.failedLogins.count} attempts`);
isCritical = securityMetrics.failedLogins.count > failedLoginThreshold * 2;
}
// Check for port scanning
if (securityMetrics.portScanning && securityMetrics.portScanning.detected) {
alerts.push(`Potential port scanning detected from ${securityMetrics.portScanning.attempts.length} sources`);
isCritical = true; // Port scanning is always considered critical
}
return {
alertRequired: alerts.length > 0,
isCritical,
alerts,
};
}