Cleanup comments and switch to integration tests

This commit is contained in:
iotux 2025-12-20 15:19:06 +01:00
parent 6aa7a74664
commit 15de932623
2 changed files with 30 additions and 112 deletions

View File

@ -18,8 +18,10 @@ class SystemServiceMonitorType extends MonitorType {
// Use the new variable name 'system_service_name' to match the monitor type change // Use the new variable name 'system_service_name' to match the monitor type change
const serviceName = monitor.system_service_name; const serviceName = monitor.system_service_name;
// Dispatch to OS-specific handler if (!serviceName) {
// Security validation is handled inside the specific methods to prevent code duplication throw new Error("Service Name is required.");
}
if (process.platform === "win32") { if (process.platform === "win32") {
return this.checkWindows(serviceName, heartbeat); return this.checkWindows(serviceName, heartbeat);
} else { } else {
@ -31,12 +33,12 @@ class SystemServiceMonitorType extends MonitorType {
* Linux Check (Systemd) * Linux Check (Systemd)
* @param {string} serviceName The name of the service to check. * @param {string} serviceName The name of the service to check.
* @param {object} heartbeat The heartbeat object. * @param {object} heartbeat The heartbeat object.
* @returns {Promise<void>} Resolves on success, rejects on error. * @returns {Promise<void>}
*/ */
async checkLinux(serviceName, heartbeat) { async checkLinux(serviceName, heartbeat) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// SECURITY: Prevent Argument Injection (e.g. passing flags like --help) // SECURITY: Prevent Argument Injection
// Only allow alphanumeric, dots, dashes, underscores, and @ (common in systemd services like user@1000) // Only allow alphanumeric, dots, dashes, underscores, and @
if (!serviceName || !/^[a-zA-Z0-9._\-@]+$/.test(serviceName)) { if (!serviceName || !/^[a-zA-Z0-9._\-@]+$/.test(serviceName)) {
heartbeat.status = DOWN; heartbeat.status = DOWN;
heartbeat.msg = "Invalid service name. Please use the internal Service Name (no spaces)."; heartbeat.msg = "Invalid service name. Please use the internal Service Name (no spaces).";
@ -44,7 +46,6 @@ class SystemServiceMonitorType extends MonitorType {
return; return;
} }
// Linter requires spaces inside array brackets: [ "arg1", "arg2" ]
execFile("systemctl", [ "is-active", serviceName ], (error, stdout, stderr) => { execFile("systemctl", [ "is-active", serviceName ], (error, stdout, stderr) => {
// Combine output and truncate to ~200 chars to prevent DB bloat // Combine output and truncate to ~200 chars to prevent DB bloat
let output = (stderr || stdout || "").toString().trim(); let output = (stderr || stdout || "").toString().trim();
@ -74,8 +75,6 @@ class SystemServiceMonitorType extends MonitorType {
async checkWindows(serviceName, heartbeat) { async checkWindows(serviceName, heartbeat) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
// SECURITY: Proper Escaping. // SECURITY: Proper Escaping.
// 1. Use Single Quotes ('${safeServiceName}') which tells PowerShell to treat the content as a pure literal string.
// 2. Escape any existing single quotes in the input by doubling them (' -> '').
const safeServiceName = serviceName.replaceAll("'", "''"); const safeServiceName = serviceName.replaceAll("'", "''");
const cmd = "powershell"; const cmd = "powershell";

View File

@ -1,124 +1,43 @@
const { describe, it, before, after, beforeEach } = require("node:test"); const { describe, it, beforeEach } = require("node:test");
const assert = require("node:assert"); const assert = require("node:assert");
const childProcess = require("child_process"); const { SystemServiceMonitorType } = require("../../server/monitor-types/system-service");
const { UP, DOWN } = require("../../src/util"); const { DOWN } = require("../../src/util");
describe("SystemServiceMonitorType", () => { describe("SystemServiceMonitorType", () => {
let SystemServiceMonitorType;
let originalExecFile;
let mockExecFileHandler;
before(() => {
// 1. Save the original execFile
originalExecFile = childProcess.execFile;
// 2. Overwrite execFile with our mock
childProcess.execFile = (file, args, callback) => {
if (mockExecFileHandler) {
mockExecFileHandler(file, args, callback);
} else {
throw new Error("mockExecFileHandler not defined");
}
};
// 3. Import the module (must happen AFTER mocking)
const module = require("../../server/monitor-types/system-service");
SystemServiceMonitorType = module.SystemServiceMonitorType;
});
after(() => {
// Restore original function
childProcess.execFile = originalExecFile;
});
let monitorType; let monitorType;
let mockHeartbeat; let heartbeat;
beforeEach(() => { beforeEach(() => {
monitorType = new SystemServiceMonitorType(); monitorType = new SystemServiceMonitorType();
mockHeartbeat = { heartbeat = {
status: DOWN, status: DOWN,
msg: "", msg: "",
}; };
mockExecFileHandler = null;
}); });
describe("Linux Checks", () => { it("should handle non-existent service gracefully", async () => {
it("should mark UP when systemctl returns active", async () => { const monitor = { system_service_name: "non-existent-service-12345" };
// Simulate Linux
Object.defineProperty(process, "platform", { value: "linux" });
mockExecFileHandler = (cmd, args, cb) => { try {
assert.strictEqual(cmd, "systemctl"); await monitorType.check(monitor, heartbeat);
assert.strictEqual(args[0], "is-active"); } catch (e) {
assert.strictEqual(args[1], "nginx"); // Expected failure for non-existent service
cb(null, "active", ""); }
};
const monitor = { system_service_name: "nginx" }; assert.strictEqual(heartbeat.status, DOWN);
await monitorType.check(monitor, mockHeartbeat); // Ensure some output was captured from the command
assert.ok(heartbeat.msg && heartbeat.msg.length > 0);
assert.strictEqual(mockHeartbeat.status, UP);
assert.ok(mockHeartbeat.msg.includes("is running"));
});
it("should mark DOWN when systemctl returns inactive", async () => {
Object.defineProperty(process, "platform", { value: "linux" });
mockExecFileHandler = (cmd, args, cb) => {
cb(new Error("Command failed"), "", "inactive");
};
const monitor = { system_service_name: "apache2" };
try {
await monitorType.check(monitor, mockHeartbeat);
} catch (e) {
// Expected error
}
assert.strictEqual(mockHeartbeat.status, DOWN);
});
}); });
describe("Windows Checks", () => { it("should fail gracefully with invalid characters", async () => {
it("should mark UP when PowerShell returns Running", async () => { const monitor = { system_service_name: "invalid&service;name" };
// Simulate Windows
Object.defineProperty(process, "platform", { value: "win32" });
let capturedCommand = ""; try {
await monitorType.check(monitor, heartbeat);
} catch (e) {
// Expected validation error
}
mockExecFileHandler = (cmd, args, cb) => { assert.strictEqual(heartbeat.status, DOWN);
const commandIndex = args.indexOf("-Command");
capturedCommand = args[commandIndex + 1];
cb(null, "Running", "");
};
const monitor = { system_service_name: "wuauserv" };
await monitorType.check(monitor, mockHeartbeat);
assert.strictEqual(mockHeartbeat.status, UP);
// Verify escaping: Must contain single quotes around service name
assert.ok(capturedCommand.includes("(Get-Service -Name 'wuauserv').Status"));
});
it("should properly escape single quotes in service names", async () => {
Object.defineProperty(process, "platform", { value: "win32" });
let capturedCommand = "";
mockExecFileHandler = (cmd, args, cb) => {
const commandIndex = args.indexOf("-Command");
capturedCommand = args[commandIndex + 1];
cb(null, "Running", "");
};
const monitor = { system_service_name: "Gary's Service" };
await monitorType.check(monitor, mockHeartbeat);
// Verify escaping: 'Gary's Service' -> 'Gary''s Service'
assert.ok(capturedCommand.includes("'Gary''s Service'"));
});
}); });
}); });