@gazzehamine/armada-watch-agent 1.4.3 → 1.4.5

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
@@ -10,11 +10,14 @@ exports.collectDockerContainers = collectDockerContainers;
10
10
  exports.collectNginxMetrics = collectNginxMetrics;
11
11
  exports.collectPM2Processes = collectPM2Processes;
12
12
  exports.collectSSLCertificates = collectSSLCertificates;
13
+ exports.collectSystemdServices = collectSystemdServices;
14
+ exports.collectSecurityData = collectSecurityData;
13
15
  const systeminformation_1 = __importDefault(require("systeminformation"));
14
16
  const os_1 = __importDefault(require("os"));
15
17
  const fs_1 = __importDefault(require("fs"));
16
18
  const child_process_1 = require("child_process");
17
19
  const util_1 = require("util");
20
+ const security_1 = require("./security");
18
21
  const execAsync = (0, util_1.promisify)(child_process_1.exec);
19
22
  let lastNetworkStats = null;
20
23
  let lastDiskStats = null;
@@ -383,3 +386,167 @@ async function collectSSLCertificates() {
383
386
  return certificates;
384
387
  }
385
388
  }
389
+ /**
390
+ * Collect systemd service information with auto-discovery
391
+ */
392
+ async function collectSystemdServices() {
393
+ try {
394
+ // Check if systemd is available
395
+ try {
396
+ await execAsync('which systemctl');
397
+ }
398
+ catch (error) {
399
+ // systemd not available (e.g., not a Linux system or using a different init system)
400
+ return [];
401
+ }
402
+ const services = [];
403
+ // Common services to monitor
404
+ const COMMON_SERVICES = [
405
+ 'nginx', 'apache2', 'httpd', // Web servers
406
+ 'docker', 'containerd', // Containers
407
+ 'postgresql', 'mysql', 'mariadb', // Databases
408
+ 'mongodb', 'redis', 'memcached',
409
+ 'ssh', 'sshd', // System services
410
+ ];
411
+ // Get all PM2 services (pm2-*)
412
+ let pm2Services = [];
413
+ try {
414
+ const { stdout } = await execAsync('systemctl list-units --type=service --all --no-pager --no-legend | grep "pm2-"');
415
+ if (stdout) {
416
+ pm2Services = stdout
417
+ .split('\n')
418
+ .filter(line => line.trim())
419
+ .map(line => line.split(/\s+/)[0].replace('.service', ''));
420
+ }
421
+ }
422
+ catch (error) {
423
+ // No PM2 services or error listing them
424
+ }
425
+ // Get all failed services
426
+ let failedServices = [];
427
+ try {
428
+ const { stdout } = await execAsync('systemctl list-units --type=service --state=failed --no-pager --no-legend');
429
+ if (stdout) {
430
+ failedServices = stdout
431
+ .split('\n')
432
+ .filter(line => line.trim())
433
+ .map(line => line.split(/\s+/)[0].replace('.service', ''));
434
+ }
435
+ }
436
+ catch (error) {
437
+ // No failed services or error listing them
438
+ }
439
+ // Combine all services to monitor (remove duplicates)
440
+ const servicesToMonitor = Array.from(new Set([...COMMON_SERVICES, ...pm2Services, ...failedServices]));
441
+ // Collect info for each service
442
+ for (const serviceName of servicesToMonitor) {
443
+ try {
444
+ const serviceInfo = await getServiceInfo(serviceName);
445
+ if (serviceInfo) {
446
+ services.push(serviceInfo);
447
+ }
448
+ }
449
+ catch (error) {
450
+ // Service doesn't exist or error getting info, skip it
451
+ continue;
452
+ }
453
+ }
454
+ return services;
455
+ }
456
+ catch (error) {
457
+ console.error('Error collecting systemd services:', error);
458
+ return [];
459
+ }
460
+ }
461
+ /**
462
+ * Get detailed information for a single systemd service
463
+ */
464
+ async function getServiceInfo(serviceName) {
465
+ try {
466
+ // Get detailed service properties
467
+ const { stdout } = await execAsync(`systemctl show ${serviceName}.service --no-pager`, { timeout: 5000 });
468
+ // Parse the output into key-value pairs
469
+ const props = {};
470
+ stdout.split('\n').forEach(line => {
471
+ const [key, ...valueParts] = line.split('=');
472
+ if (key && valueParts.length > 0) {
473
+ props[key.trim()] = valueParts.join('=').trim();
474
+ }
475
+ });
476
+ // Check if service exists
477
+ if (props.LoadState === 'not-found') {
478
+ return null;
479
+ }
480
+ // Extract service information
481
+ const activeState = props.ActiveState || 'unknown';
482
+ const subState = props.SubState || 'unknown';
483
+ const unitFileState = props.UnitFileState || 'unknown';
484
+ const enabled = ['enabled', 'enabled-runtime', 'static'].includes(unitFileState);
485
+ // Parse timestamps
486
+ const activeEnterTimestamp = props.ActiveEnterTimestamp ? new Date(props.ActiveEnterTimestamp).getTime() : 0;
487
+ const now = Date.now();
488
+ // Calculate uptime (in seconds)
489
+ let uptime = 0;
490
+ if (activeState === 'active' && activeEnterTimestamp > 0) {
491
+ uptime = Math.floor((now - activeEnterTimestamp) / 1000);
492
+ }
493
+ // Get restart count
494
+ const restartCount = parseInt(props.NRestarts || '0', 10);
495
+ // Get main PID
496
+ const mainPID = parseInt(props.MainPID || '0', 10);
497
+ // Get CPU and Memory usage
498
+ let cpuPercent = 0;
499
+ let memoryBytes = 0;
500
+ if (mainPID > 0) {
501
+ try {
502
+ // Use ps to get CPU and memory for the main PID
503
+ const { stdout: psOutput } = await execAsync(`ps -p ${mainPID} -o %cpu=,%mem=,rss= --no-headers`, { timeout: 2000 });
504
+ if (psOutput) {
505
+ const parts = psOutput.trim().split(/\s+/);
506
+ if (parts.length >= 3) {
507
+ cpuPercent = parseFloat(parts[0]) || 0;
508
+ // RSS is in KB, convert to bytes
509
+ memoryBytes = (parseInt(parts[2], 10) || 0) * 1024;
510
+ }
511
+ }
512
+ }
513
+ catch (error) {
514
+ // Process might have ended, use 0 values
515
+ }
516
+ }
517
+ return {
518
+ name: serviceName,
519
+ status: activeState,
520
+ subState: subState,
521
+ enabled: enabled,
522
+ uptime: uptime,
523
+ startedAt: activeEnterTimestamp,
524
+ restartCount: restartCount,
525
+ cpuPercent: cpuPercent,
526
+ memoryBytes: memoryBytes,
527
+ pid: mainPID,
528
+ };
529
+ }
530
+ catch (error) {
531
+ // Service doesn't exist or error getting info
532
+ return null;
533
+ }
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
@@ -77,6 +77,7 @@ async function sendMetrics() {
77
77
  const dockerContainers = await (0, collector_1.collectDockerContainers)();
78
78
  const pm2Processes = await (0, collector_1.collectPM2Processes)();
79
79
  const nginxMetrics = await (0, collector_1.collectNginxMetrics)();
80
+ const systemdServices = await (0, collector_1.collectSystemdServices)();
80
81
  // Check if it's time to refresh SSL certificates (every 5 minutes)
81
82
  const now = Date.now();
82
83
  const shouldCheckSSL = (now - lastSSLCheckTime) >= SSL_CHECK_INTERVAL;
@@ -97,6 +98,7 @@ async function sendMetrics() {
97
98
  dockerContainers,
98
99
  pm2Processes: pm2Processes.length > 0 ? pm2Processes : undefined,
99
100
  nginxMetrics: nginxMetrics || undefined,
101
+ systemdServices: systemdServices.length > 0 ? systemdServices : undefined,
100
102
  // Always send SSL certificates if we have them (from startup or last check)
101
103
  sslCertificates: lastSSLCertificates.length > 0 ? lastSSLCertificates : undefined,
102
104
  };
@@ -105,7 +107,8 @@ async function sendMetrics() {
105
107
  });
106
108
  const pm2Info = pm2Processes.length > 0 ? ` | PM2: ${pm2Processes.length}` : '';
107
109
  const nginxInfo = nginxMetrics ? ` | Nginx: ${nginxMetrics.activeConnections} conn` : '';
108
- console.log(`✓ Metrics sent successfully - CPU: ${metrics.cpuUsage.toFixed(1)}% | Memory: ${metrics.memoryUsage.toFixed(1)}%${pm2Info}${nginxInfo}`);
110
+ const systemdInfo = systemdServices.length > 0 ? ` | Services: ${systemdServices.length}` : '';
111
+ console.log(`✓ Metrics sent successfully - CPU: ${metrics.cpuUsage.toFixed(1)}% | Memory: ${metrics.memoryUsage.toFixed(1)}%${pm2Info}${nginxInfo}${systemdInfo}`);
109
112
  }
110
113
  catch (error) {
111
114
  if (axios_1.default.isAxiosError(error)) {
@@ -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.3",
4
- "description": "Monitoring agent for Armada Watch - EC2 instance monitoring with SSL, PM2, and Nginx monitoring",
3
+ "version": "1.4.5",
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"