@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 +20 -0
- package/dist/index.js +15 -0
- package/dist/security/failed-logins.js +135 -0
- package/dist/security/file-integrity.js +101 -0
- package/dist/security/index.js +33 -0
- package/dist/security/open-ports.js +169 -0
- package/dist/security/user-audit.js +206 -0
- package/package.json +2 -2
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
|
-
"description": "Monitoring agent for Armada Watch - EC2 instance monitoring with SSL, PM2, Nginx, and
|
|
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"
|