@gazzehamine/armada-watch-agent 1.4.4 → 1.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/collector.js CHANGED
@@ -11,11 +11,13 @@ exports.collectNginxMetrics = collectNginxMetrics;
11
11
  exports.collectPM2Processes = collectPM2Processes;
12
12
  exports.collectSSLCertificates = collectSSLCertificates;
13
13
  exports.collectSystemdServices = collectSystemdServices;
14
+ exports.collectSecurityData = collectSecurityData;
14
15
  const systeminformation_1 = __importDefault(require("systeminformation"));
15
16
  const os_1 = __importDefault(require("os"));
16
17
  const fs_1 = __importDefault(require("fs"));
17
18
  const child_process_1 = require("child_process");
18
19
  const util_1 = require("util");
20
+ const security_1 = require("./security");
19
21
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
20
22
  let lastNetworkStats = null;
21
23
  let lastDiskStats = null;
@@ -530,3 +532,21 @@ async function getServiceInfo(serviceName) {
530
532
  return null;
531
533
  }
532
534
  }
535
+ /**
536
+ * Collect security monitoring data
537
+ */
538
+ async function collectSecurityData() {
539
+ try {
540
+ const securityData = await (0, security_1.collectAllSecurityMetrics)();
541
+ return securityData;
542
+ }
543
+ catch (error) {
544
+ console.error('Error collecting security data:', error);
545
+ return {
546
+ failedLogins: null,
547
+ openPorts: [],
548
+ fileIntegrity: [],
549
+ userAudit: null,
550
+ };
551
+ }
552
+ }
package/dist/index.js CHANGED
@@ -30,6 +30,9 @@ let instanceInfo = null;
30
30
  let lastSSLCertificates = [];
31
31
  let lastSSLCheckTime = 0;
32
32
  const SSL_CHECK_INTERVAL = 5 * 60 * 1000; // Check SSL every 5 minutes
33
+ let lastSecurityData = null;
34
+ let lastSecurityCheckTime = 0;
35
+ const SECURITY_CHECK_INTERVAL = 60 * 1000; // Check security every 60 seconds
33
36
  async function initializeAgent() {
34
37
  try {
35
38
  console.log("🔄 Initializing Armada Watch Agent...");
@@ -88,6 +91,16 @@ async function sendMetrics() {
88
91
  console.log(`🔐 SSL Certificates refreshed: ${lastSSLCertificates.length} domain(s)`);
89
92
  }
90
93
  }
94
+ // Check if it's time to refresh security data (every 60 seconds)
95
+ const shouldCheckSecurity = (now - lastSecurityCheckTime) >= SECURITY_CHECK_INTERVAL;
96
+ if (shouldCheckSecurity) {
97
+ lastSecurityData = await (0, collector_1.collectSecurityData)();
98
+ lastSecurityCheckTime = now;
99
+ const failedCount = lastSecurityData.failedLogins?.lastHour || 0;
100
+ const portsCount = lastSecurityData.openPorts?.length || 0;
101
+ const sessionsCount = lastSecurityData.userAudit?.activeSessions?.length || 0;
102
+ console.log(`🔒 Security data refreshed: ${failedCount} failed logins, ${portsCount} open ports, ${sessionsCount} active sessions`);
103
+ }
91
104
  const payload = {
92
105
  instanceInfo,
93
106
  metrics: {
@@ -101,6 +114,8 @@ async function sendMetrics() {
101
114
  systemdServices: systemdServices.length > 0 ? systemdServices : undefined,
102
115
  // Always send SSL certificates if we have them (from startup or last check)
103
116
  sslCertificates: lastSSLCertificates.length > 0 ? lastSSLCertificates : undefined,
117
+ // Send security data if we have it
118
+ securityData: lastSecurityData || undefined,
104
119
  };
105
120
  await axios_1.default.post(`${SERVER_URL}/api/metrics`, payload, {
106
121
  timeout: 5000,
@@ -0,0 +1,135 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.collectFailedLogins = collectFailedLogins;
7
+ const child_process_1 = require("child_process");
8
+ const util_1 = require("util");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
11
+ /**
12
+ * Collect failed login attempts from auth.log
13
+ */
14
+ async function collectFailedLogins() {
15
+ try {
16
+ // Check which log file exists
17
+ const authLogPath = await getAuthLogPath();
18
+ if (!authLogPath) {
19
+ return null; // No auth logs available
20
+ }
21
+ // Read last 1000 lines from auth log
22
+ const { stdout } = await execAsync(`tail -n 1000 ${authLogPath}`, { timeout: 5000 });
23
+ const lines = stdout.split('\n');
24
+ const failedAttempts = [];
25
+ const now = Date.now();
26
+ const oneHourAgo = now - (60 * 60 * 1000);
27
+ const oneDayAgo = now - (24 * 60 * 60 * 1000);
28
+ for (const line of lines) {
29
+ // Match SSH failed password attempts
30
+ // Example: "Jan 21 12:00:00 hostname sshd[12345]: Failed password for invalid user admin from 192.168.1.100 port 54321 ssh2"
31
+ const sshFailedMatch = line.match(/(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}).*sshd.*Failed password for (?:invalid user )?(\w+) from ([\d.]+) port (\d+)/);
32
+ if (sshFailedMatch) {
33
+ const [, dateStr, username, ip, port] = sshFailedMatch;
34
+ const timestamp = parseAuthLogDate(dateStr);
35
+ failedAttempts.push({
36
+ timestamp,
37
+ username,
38
+ ipAddress: ip,
39
+ port: parseInt(port, 10),
40
+ method: 'ssh',
41
+ });
42
+ }
43
+ // Match authentication failure
44
+ const authFailureMatch = line.match(/(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}).*authentication failure.*user=(\w+).*rhost=([\d.]+)/);
45
+ if (authFailureMatch) {
46
+ const [, dateStr, username, ip] = authFailureMatch;
47
+ const timestamp = parseAuthLogDate(dateStr);
48
+ failedAttempts.push({
49
+ timestamp,
50
+ username,
51
+ ipAddress: ip,
52
+ port: 0,
53
+ method: 'auth',
54
+ });
55
+ }
56
+ }
57
+ // Calculate statistics
58
+ const last24hAttempts = failedAttempts.filter(a => a.timestamp.getTime() > oneDayAgo);
59
+ const lastHourAttempts = failedAttempts.filter(a => a.timestamp.getTime() > oneHourAgo);
60
+ // Count by IP
61
+ const ipCounts = new Map();
62
+ for (const attempt of last24hAttempts) {
63
+ ipCounts.set(attempt.ipAddress, (ipCounts.get(attempt.ipAddress) || 0) + 1);
64
+ }
65
+ // Count by username
66
+ const userCounts = new Map();
67
+ for (const attempt of last24hAttempts) {
68
+ userCounts.set(attempt.username, (userCounts.get(attempt.username) || 0) + 1);
69
+ }
70
+ // Get top attackers
71
+ const topAttackers = Array.from(ipCounts.entries())
72
+ .map(([ip, count]) => ({ ip, count }))
73
+ .sort((a, b) => b.count - a.count)
74
+ .slice(0, 10);
75
+ // Get top targeted users
76
+ const topTargetedUsers = Array.from(userCounts.entries())
77
+ .map(([username, count]) => ({ username, count }))
78
+ .sort((a, b) => b.count - a.count)
79
+ .slice(0, 10);
80
+ return {
81
+ totalFailures: failedAttempts.length,
82
+ last24h: last24hAttempts.length,
83
+ lastHour: lastHourAttempts.length,
84
+ uniqueIPs: ipCounts.size,
85
+ topAttackers,
86
+ topTargetedUsers,
87
+ recentAttempts: lastHourAttempts.slice(0, 50), // Last 50 attempts in the hour
88
+ };
89
+ }
90
+ catch (error) {
91
+ console.error('Error collecting failed logins:', error);
92
+ return null;
93
+ }
94
+ }
95
+ /**
96
+ * Get the path to the auth log file
97
+ */
98
+ async function getAuthLogPath() {
99
+ // Try Ubuntu/Debian path
100
+ if (fs_1.default.existsSync('/var/log/auth.log')) {
101
+ try {
102
+ await fs_1.default.promises.access('/var/log/auth.log', fs_1.default.constants.R_OK);
103
+ return '/var/log/auth.log';
104
+ }
105
+ catch {
106
+ return null; // Not readable
107
+ }
108
+ }
109
+ // Try RHEL/CentOS path
110
+ if (fs_1.default.existsSync('/var/log/secure')) {
111
+ try {
112
+ await fs_1.default.promises.access('/var/log/secure', fs_1.default.constants.R_OK);
113
+ return '/var/log/secure';
114
+ }
115
+ catch {
116
+ return null; // Not readable
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+ /**
122
+ * Parse auth.log date format (without year)
123
+ * Example: "Jan 21 12:00:00"
124
+ */
125
+ function parseAuthLogDate(dateStr) {
126
+ const currentYear = new Date().getFullYear();
127
+ const dateWithYear = `${dateStr} ${currentYear}`;
128
+ // Parse the date
129
+ const date = new Date(dateWithYear);
130
+ // If the parsed date is in the future, it's probably from last year
131
+ if (date > new Date()) {
132
+ date.setFullYear(currentYear - 1);
133
+ }
134
+ return date;
135
+ }
@@ -0,0 +1,101 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.collectFileIntegrity = collectFileIntegrity;
7
+ exports.hasFileChanged = hasFileChanged;
8
+ const crypto_1 = require("crypto");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const promises_1 = require("fs/promises");
11
+ // Files to monitor (world-readable files only)
12
+ const MONITORED_FILES = [
13
+ '/etc/passwd',
14
+ '/etc/group',
15
+ '/etc/hosts',
16
+ '/etc/hostname',
17
+ '/etc/ssh/sshd_config', // May or may not be readable
18
+ ];
19
+ /**
20
+ * Collect file integrity information
21
+ */
22
+ async function collectFileIntegrity() {
23
+ const files = [];
24
+ for (const filePath of MONITORED_FILES) {
25
+ try {
26
+ // Check if file exists and is readable
27
+ if (!fs_1.default.existsSync(filePath)) {
28
+ continue;
29
+ }
30
+ await fs_1.default.promises.access(filePath, fs_1.default.constants.R_OK);
31
+ // Get file stats
32
+ const stats = await (0, promises_1.stat)(filePath);
33
+ // Read file content
34
+ const content = await fs_1.default.promises.readFile(filePath);
35
+ // Calculate SHA-256 hash
36
+ const hash = (0, crypto_1.createHash)('sha256').update(content).digest('hex');
37
+ // Get file permissions in octal format
38
+ const permissions = (stats.mode & parseInt('777', 8)).toString(8);
39
+ // Get owner/group (requires parsing ls -l output or using uid/gid)
40
+ const { owner, group } = await getFileOwnership(filePath);
41
+ files.push({
42
+ path: filePath,
43
+ hash,
44
+ lastModified: stats.mtime,
45
+ size: stats.size,
46
+ permissions,
47
+ owner,
48
+ group,
49
+ });
50
+ }
51
+ catch (error) {
52
+ // File not readable or doesn't exist, skip it
53
+ continue;
54
+ }
55
+ }
56
+ return files;
57
+ }
58
+ /**
59
+ * Get file ownership information
60
+ */
61
+ async function getFileOwnership(filePath) {
62
+ try {
63
+ const stats = await (0, promises_1.stat)(filePath);
64
+ // Try to resolve UID/GID to names
65
+ try {
66
+ const { exec } = require('child_process');
67
+ const { promisify } = require('util');
68
+ const execAsync = promisify(exec);
69
+ const { stdout } = await execAsync(`ls -ld ${filePath}`);
70
+ const parts = stdout.trim().split(/\s+/);
71
+ if (parts.length >= 3) {
72
+ return {
73
+ owner: parts[2] || stats.uid.toString(),
74
+ group: parts[3] || stats.gid.toString(),
75
+ };
76
+ }
77
+ }
78
+ catch {
79
+ // Fallback to numeric IDs
80
+ }
81
+ return {
82
+ owner: stats.uid.toString(),
83
+ group: stats.gid.toString(),
84
+ };
85
+ }
86
+ catch (error) {
87
+ return {
88
+ owner: 'unknown',
89
+ group: 'unknown',
90
+ };
91
+ }
92
+ }
93
+ /**
94
+ * Check if file has been modified (compare with baseline)
95
+ */
96
+ function hasFileChanged(current, baseline) {
97
+ if (!baseline) {
98
+ return false; // No baseline to compare
99
+ }
100
+ return current.hash !== baseline.hash;
101
+ }
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectUserAudit = exports.hasFileChanged = exports.collectFileIntegrity = exports.isExpectedPort = exports.getPortDescription = exports.collectOpenPorts = exports.collectFailedLogins = void 0;
4
+ exports.collectAllSecurityMetrics = collectAllSecurityMetrics;
5
+ // Export all security collectors
6
+ const failed_logins_1 = require("./failed-logins");
7
+ Object.defineProperty(exports, "collectFailedLogins", { enumerable: true, get: function () { return failed_logins_1.collectFailedLogins; } });
8
+ const open_ports_1 = require("./open-ports");
9
+ Object.defineProperty(exports, "collectOpenPorts", { enumerable: true, get: function () { return open_ports_1.collectOpenPorts; } });
10
+ Object.defineProperty(exports, "getPortDescription", { enumerable: true, get: function () { return open_ports_1.getPortDescription; } });
11
+ Object.defineProperty(exports, "isExpectedPort", { enumerable: true, get: function () { return open_ports_1.isExpectedPort; } });
12
+ const file_integrity_1 = require("./file-integrity");
13
+ Object.defineProperty(exports, "collectFileIntegrity", { enumerable: true, get: function () { return file_integrity_1.collectFileIntegrity; } });
14
+ Object.defineProperty(exports, "hasFileChanged", { enumerable: true, get: function () { return file_integrity_1.hasFileChanged; } });
15
+ const user_audit_1 = require("./user-audit");
16
+ Object.defineProperty(exports, "collectUserAudit", { enumerable: true, get: function () { return user_audit_1.collectUserAudit; } });
17
+ /**
18
+ * Collect all security metrics
19
+ */
20
+ async function collectAllSecurityMetrics() {
21
+ const [failedLogins, openPorts, fileIntegrity, userAudit] = await Promise.all([
22
+ (0, failed_logins_1.collectFailedLogins)().catch(() => null),
23
+ (0, open_ports_1.collectOpenPorts)().catch(() => []),
24
+ (0, file_integrity_1.collectFileIntegrity)().catch(() => []),
25
+ (0, user_audit_1.collectUserAudit)().catch(() => null),
26
+ ]);
27
+ return {
28
+ failedLogins,
29
+ openPorts,
30
+ fileIntegrity,
31
+ userAudit,
32
+ };
33
+ }
@@ -0,0 +1,169 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectOpenPorts = collectOpenPorts;
4
+ exports.getPortDescription = getPortDescription;
5
+ exports.isExpectedPort = isExpectedPort;
6
+ const child_process_1 = require("child_process");
7
+ const util_1 = require("util");
8
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
9
+ /**
10
+ * Collect open/listening ports
11
+ */
12
+ async function collectOpenPorts() {
13
+ try {
14
+ // Use ss command (modern replacement for netstat)
15
+ // -tuln: tcp, udp, listening, numeric
16
+ const { stdout } = await execAsync('ss -tuln', { timeout: 5000 });
17
+ const lines = stdout.split('\n');
18
+ const ports = [];
19
+ for (const line of lines) {
20
+ // Skip header and empty lines
21
+ if (!line || line.startsWith('Netid') || line.startsWith('State')) {
22
+ continue;
23
+ }
24
+ // Parse ss output
25
+ // Format: Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
26
+ // Example: tcp LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
27
+ const parts = line.trim().split(/\s+/);
28
+ if (parts.length < 5) {
29
+ continue;
30
+ }
31
+ const [netid, state, , , localAddr] = parts;
32
+ // Parse protocol
33
+ let protocol;
34
+ if (netid.startsWith('tcp')) {
35
+ protocol = 'tcp';
36
+ }
37
+ else if (netid.startsWith('udp')) {
38
+ protocol = 'udp';
39
+ }
40
+ else {
41
+ continue; // Skip other protocols
42
+ }
43
+ // Parse local address and port
44
+ const lastColon = localAddr.lastIndexOf(':');
45
+ if (lastColon === -1) {
46
+ continue;
47
+ }
48
+ const address = localAddr.substring(0, lastColon);
49
+ const portStr = localAddr.substring(lastColon + 1);
50
+ const port = parseInt(portStr, 10);
51
+ if (isNaN(port)) {
52
+ continue;
53
+ }
54
+ ports.push({
55
+ port,
56
+ protocol,
57
+ state: state || 'LISTEN',
58
+ localAddress: address,
59
+ });
60
+ }
61
+ // Sort by port number
62
+ return ports.sort((a, b) => a.port - b.port);
63
+ }
64
+ catch (error) {
65
+ console.error('Error collecting open ports:', error);
66
+ // Fallback to netstat if ss is not available
67
+ try {
68
+ return await collectOpenPortsNetstat();
69
+ }
70
+ catch (fallbackError) {
71
+ console.error('Error with netstat fallback:', fallbackError);
72
+ return [];
73
+ }
74
+ }
75
+ }
76
+ /**
77
+ * Fallback to netstat if ss is not available
78
+ */
79
+ async function collectOpenPortsNetstat() {
80
+ const { stdout } = await execAsync('netstat -tuln', { timeout: 5000 });
81
+ const lines = stdout.split('\n');
82
+ const ports = [];
83
+ for (const line of lines) {
84
+ // Skip header and empty lines
85
+ if (!line || line.startsWith('Active') || line.startsWith('Proto')) {
86
+ continue;
87
+ }
88
+ // Parse netstat output
89
+ // Format: Proto Recv-Q Send-Q Local Address Foreign Address State
90
+ // Example: tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
91
+ const parts = line.trim().split(/\s+/);
92
+ if (parts.length < 4) {
93
+ continue;
94
+ }
95
+ const [proto, , , localAddr, , state] = parts;
96
+ // Parse protocol
97
+ let protocol;
98
+ if (proto.startsWith('tcp')) {
99
+ protocol = 'tcp';
100
+ }
101
+ else if (proto.startsWith('udp')) {
102
+ protocol = 'udp';
103
+ }
104
+ else {
105
+ continue;
106
+ }
107
+ // Parse local address and port
108
+ const lastColon = localAddr.lastIndexOf(':');
109
+ if (lastColon === -1) {
110
+ continue;
111
+ }
112
+ const address = localAddr.substring(0, lastColon);
113
+ const portStr = localAddr.substring(lastColon + 1);
114
+ const port = parseInt(portStr, 10);
115
+ if (isNaN(port)) {
116
+ continue;
117
+ }
118
+ ports.push({
119
+ port,
120
+ protocol,
121
+ state: state || 'LISTEN',
122
+ localAddress: address,
123
+ });
124
+ }
125
+ return ports.sort((a, b) => a.port - b.port);
126
+ }
127
+ /**
128
+ * Get common port descriptions
129
+ */
130
+ function getPortDescription(port) {
131
+ const commonPorts = {
132
+ 20: 'FTP Data',
133
+ 21: 'FTP Control',
134
+ 22: 'SSH',
135
+ 23: 'Telnet',
136
+ 25: 'SMTP',
137
+ 53: 'DNS',
138
+ 80: 'HTTP',
139
+ 110: 'POP3',
140
+ 143: 'IMAP',
141
+ 443: 'HTTPS',
142
+ 465: 'SMTPS',
143
+ 587: 'SMTP Submission',
144
+ 993: 'IMAPS',
145
+ 995: 'POP3S',
146
+ 3000: 'Development Server',
147
+ 3306: 'MySQL',
148
+ 5432: 'PostgreSQL',
149
+ 6379: 'Redis',
150
+ 8080: 'HTTP Alternate',
151
+ 8443: 'HTTPS Alternate',
152
+ 27017: 'MongoDB',
153
+ };
154
+ return commonPorts[port] || 'Unknown';
155
+ }
156
+ /**
157
+ * Check if a port is considered "expected"
158
+ */
159
+ function isExpectedPort(port) {
160
+ const expectedPorts = [
161
+ 22, // SSH
162
+ 80, // HTTP
163
+ 443, // HTTPS
164
+ 3000, // Common dev server
165
+ 4000, // Backend API
166
+ 8080, // HTTP alternate
167
+ ];
168
+ return expectedPorts.includes(port);
169
+ }
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.collectUserAudit = collectUserAudit;
7
+ const child_process_1 = require("child_process");
8
+ const util_1 = require("util");
9
+ const fs_1 = __importDefault(require("fs"));
10
+ const execAsync = (0, util_1.promisify)(child_process_1.exec);
11
+ /**
12
+ * Collect user audit information
13
+ */
14
+ async function collectUserAudit() {
15
+ const users = await collectUsers();
16
+ const activeSessions = await collectActiveSessions();
17
+ const humanUsers = users.filter(u => !u.isSystemUser);
18
+ const systemUsers = users.filter(u => u.isSystemUser);
19
+ return {
20
+ totalUsers: users.length,
21
+ humanUsers: humanUsers.length,
22
+ systemUsers: systemUsers.length,
23
+ users,
24
+ activeSessions,
25
+ };
26
+ }
27
+ /**
28
+ * Parse /etc/passwd to get user information
29
+ */
30
+ async function collectUsers() {
31
+ try {
32
+ const passwdContent = await fs_1.default.promises.readFile('/etc/passwd', 'utf-8');
33
+ const lines = passwdContent.split('\n').filter(l => l.trim());
34
+ const users = [];
35
+ for (const line of lines) {
36
+ // Format: username:x:uid:gid:comment:home:shell
37
+ const parts = line.split(':');
38
+ if (parts.length < 7)
39
+ continue;
40
+ const [username, , uidStr, gidStr, , homeDir, shell] = parts;
41
+ const uid = parseInt(uidStr, 10);
42
+ const gid = parseInt(gidStr, 10);
43
+ // Get user's groups
44
+ const groups = await getUserGroups(username);
45
+ users.push({
46
+ username,
47
+ uid,
48
+ gid,
49
+ shell,
50
+ homeDir,
51
+ groups,
52
+ isSystemUser: uid < 1000,
53
+ });
54
+ }
55
+ return users;
56
+ }
57
+ catch (error) {
58
+ console.error('Error collecting users:', error);
59
+ return [];
60
+ }
61
+ }
62
+ /**
63
+ * Get groups for a user
64
+ */
65
+ async function getUserGroups(username) {
66
+ try {
67
+ const { stdout } = await execAsync(`groups ${username}`, { timeout: 2000 });
68
+ // Output format: "username : group1 group2 group3"
69
+ const parts = stdout.trim().split(':');
70
+ if (parts.length < 2)
71
+ return [];
72
+ return parts[1].trim().split(/\s+/);
73
+ }
74
+ catch (error) {
75
+ return [];
76
+ }
77
+ }
78
+ /**
79
+ * Get currently active login sessions
80
+ */
81
+ async function collectActiveSessions() {
82
+ try {
83
+ // Use 'w' command to get active sessions
84
+ // Output format:
85
+ // USER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT
86
+ // ubuntu pts/0 192.168.1.100 10:30 0.00s 0.01s 0.00s w
87
+ const { stdout } = await execAsync('w -h', { timeout: 2000 });
88
+ const lines = stdout.split('\n').filter(l => l.trim());
89
+ const sessions = [];
90
+ for (const line of lines) {
91
+ const parts = line.trim().split(/\s+/);
92
+ if (parts.length < 4)
93
+ continue;
94
+ const [username, terminal, from, loginTimeStr, idleStr] = parts;
95
+ // Parse login time
96
+ const loginTime = parseLoginTime(loginTimeStr);
97
+ // Parse idle time
98
+ const idle = parseIdleTime(idleStr || '0');
99
+ sessions.push({
100
+ username,
101
+ from: from === '-' ? 'local' : from,
102
+ loginTime,
103
+ terminal,
104
+ idle,
105
+ });
106
+ }
107
+ return sessions;
108
+ }
109
+ catch (error) {
110
+ console.error('Error collecting active sessions:', error);
111
+ // Fallback to 'who' command
112
+ try {
113
+ return await collectActiveSessionsWho();
114
+ }
115
+ catch {
116
+ return [];
117
+ }
118
+ }
119
+ }
120
+ /**
121
+ * Fallback using 'who' command
122
+ */
123
+ async function collectActiveSessionsWho() {
124
+ const { stdout } = await execAsync('who', { timeout: 2000 });
125
+ const lines = stdout.split('\n').filter(l => l.trim());
126
+ const sessions = [];
127
+ for (const line of lines) {
128
+ // Format: ubuntu pts/0 2026-01-21 10:30 (192.168.1.100)
129
+ const match = line.match(/^(\S+)\s+(\S+)\s+(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2})\s*\(?([\d.]*)\)?/);
130
+ if (match) {
131
+ const [, username, terminal, timeStr, from] = match;
132
+ const loginTime = new Date(timeStr);
133
+ sessions.push({
134
+ username,
135
+ from: from || 'local',
136
+ loginTime,
137
+ terminal,
138
+ idle: 0,
139
+ });
140
+ }
141
+ }
142
+ return sessions;
143
+ }
144
+ /**
145
+ * Parse login time from 'w' command output
146
+ * Formats: "10:30" (today), "21Jan24" (specific date), "5days" (days ago)
147
+ */
148
+ function parseLoginTime(timeStr) {
149
+ const now = new Date();
150
+ // Format: HH:MM (today)
151
+ if (timeStr.includes(':')) {
152
+ const [hours, minutes] = timeStr.split(':').map(Number);
153
+ const loginTime = new Date();
154
+ loginTime.setHours(hours, minutes, 0, 0);
155
+ return loginTime;
156
+ }
157
+ // Format: DDMmmYY (specific date)
158
+ if (timeStr.length >= 7) {
159
+ // Parse date like "21Jan24"
160
+ const day = parseInt(timeStr.substring(0, 2), 10);
161
+ const monthStr = timeStr.substring(2, 5);
162
+ const year = 2000 + parseInt(timeStr.substring(5), 10);
163
+ const months = {
164
+ Jan: 0, Feb: 1, Mar: 2, Apr: 3, May: 4, Jun: 5,
165
+ Jul: 6, Aug: 7, Sep: 8, Oct: 9, Nov: 10, Dec: 11,
166
+ };
167
+ const month = months[monthStr];
168
+ if (month !== undefined) {
169
+ return new Date(year, month, day);
170
+ }
171
+ }
172
+ // Format: "5days" (days ago)
173
+ if (timeStr.includes('day')) {
174
+ const days = parseInt(timeStr, 10);
175
+ const loginTime = new Date();
176
+ loginTime.setDate(loginTime.getDate() - days);
177
+ return loginTime;
178
+ }
179
+ // Default to now
180
+ return now;
181
+ }
182
+ /**
183
+ * Parse idle time from 'w' command output
184
+ * Formats: "0.00s", "1:30", "5days"
185
+ */
186
+ function parseIdleTime(idleStr) {
187
+ if (idleStr.includes('s')) {
188
+ // Seconds
189
+ return parseFloat(idleStr);
190
+ }
191
+ if (idleStr.includes(':')) {
192
+ // Minutes:seconds
193
+ const [mins, secs] = idleStr.split(':').map(Number);
194
+ return mins * 60 + (secs || 0);
195
+ }
196
+ if (idleStr.includes('day')) {
197
+ // Days
198
+ const days = parseInt(idleStr, 10);
199
+ return days * 24 * 60 * 60;
200
+ }
201
+ if (idleStr.includes('m')) {
202
+ // Minutes
203
+ return parseInt(idleStr, 10) * 60;
204
+ }
205
+ return 0;
206
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gazzehamine/armada-watch-agent",
3
- "version": "1.4.4",
4
- "description": "Monitoring agent for Armada Watch - EC2 instance monitoring with SSL, PM2, Nginx, and Systemd monitoring",
3
+ "version": "1.4.6",
4
+ "description": "Monitoring agent for Armada Watch - EC2 instance monitoring with SSL, PM2, Nginx, Systemd, and Security monitoring",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "armada-watch-agent": "dist/index.js"