From d76ce4e28df2a50fe878cac6935d010c8ba467cc Mon Sep 17 00:00:00 2001 From: iotux Date: Sat, 13 Dec 2025 15:25:14 +0100 Subject: [PATCH] Simplified and secured Local Service monitor --- ...25-12-09-0000-add-local-service-monitor.js | 22 ++---- server/model/monitor.js | 4 +- server/monitor-types/local-service.js | 70 ++++++++----------- server/server.js | 4 +- src/lang/en.json | 4 +- src/pages/EditMonitor.vue | 57 ++------------- 6 files changed, 41 insertions(+), 120 deletions(-) diff --git a/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js b/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js index deb411040..ef627a324 100644 --- a/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js +++ b/db/knex_migrations/2025-12-09-0000-add-local-service-monitor.js @@ -4,9 +4,7 @@ */ exports.up = async (knex) => { await knex.schema.alterTable("monitor", (table) => { - table.string("local_service_command"); - table.string("local_service_expected_output"); - table.string("local_service_check_type").notNullable().defaultTo("keyword"); + table.string("local_service_name"); }); }; @@ -15,19 +13,7 @@ exports.up = async (knex) => { * @returns {Promise} */ exports.down = async (knex) => { - if (await knex.schema.hasColumn("monitor", "local_service_command")) { - await knex.schema.alterTable("monitor", (table) => { - table.dropColumn("local_service_command"); - }); - } - if (await knex.schema.hasColumn("monitor", "local_service_expected_output")) { - await knex.schema.alterTable("monitor", (table) => { - table.dropColumn("local_service_expected_output"); - }); - } - if (await knex.schema.hasColumn("monitor", "local_service_check_type")) { - await knex.schema.alterTable("monitor", (table) => { - table.dropColumn("local_service_check_type"); - }); - } + await knex.schema.alterTable("monitor", (table) => { + table.dropColumn("local_service_name"); + }); }; diff --git a/server/model/monitor.js b/server/model/monitor.js index 08c83f4ee..c9ba0b140 100644 --- a/server/model/monitor.js +++ b/server/model/monitor.js @@ -148,7 +148,7 @@ class Monitor extends BeanModel { httpBodyEncoding: this.httpBodyEncoding, jsonPath: this.jsonPath, expectedValue: this.expectedValue, - local_service_check_type: this.local_service_check_type, + local_service_name: this.local_service_name, kafkaProducerTopic: this.kafkaProducerTopic, kafkaProducerBrokers: JSON.parse(this.kafkaProducerBrokers), kafkaProducerSsl: this.getKafkaProducerSsl(), @@ -202,8 +202,6 @@ class Monitor extends BeanModel { kafkaProducerSaslOptions: JSON.parse(this.kafkaProducerSaslOptions), rabbitmqUsername: this.rabbitmqUsername, rabbitmqPassword: this.rabbitmqPassword, - local_service_command: this.local_service_command, - local_service_expected_output: this.local_service_expected_output, }; } diff --git a/server/monitor-types/local-service.js b/server/monitor-types/local-service.js index d5eaeb849..65183fda0 100644 --- a/server/monitor-types/local-service.js +++ b/server/monitor-types/local-service.js @@ -1,60 +1,48 @@ const { MonitorType } = require("./monitor-type"); -const { exec } = require("child_process"); -const { DOWN, UP, evaluateJsonQuery } = require("../../src/util"); +const { execFile } = require("child_process"); +const { DOWN, UP } = require("../../src/util"); class LocalServiceMonitorType extends MonitorType { name = "local-service"; description = "Checks if a local service is running by executing a command."; /** - * @inheritdoc + * Check a local systemd service status. + * Uses `systemctl is-running` to determine if the service is active. + * @param {object} monitor The monitor object containing serviceName. + * @param {object} heartbeat The heartbeat object to update. + * @param {object} server The server object (unused in this specific check). + * @returns {Promise} A promise that resolves with the updated heartbeat. + * @throws {Error} If the serviceName is invalid or the command execution fails. */ async check(monitor, heartbeat, server) { + // This is the name of the service to check e.g. "nginx.service" + const serviceName = monitor.local_service_name; + + // Basic sanitization to prevent argument injection. + // This regex allows for standard service names, including those with instances like "sshd@.service". + if (!serviceName || !/^[a-zA-Z0-9._\-@]+$/.test(serviceName)) { + heartbeat.status = DOWN; + heartbeat.msg = "Invalid service name provided."; + throw new Error(heartbeat.msg); + } + return new Promise((resolve, reject) => { - exec(monitor.local_service_command, async (error, stdout, stderr) => { + execFile("systemctl", [ "is-active", serviceName ], (error, stdout, stderr) => { + // systemctl is-active exits with 0 if the service is active, + // and a non-zero code if it is inactive, failed, or not found. if (error) { heartbeat.status = DOWN; - heartbeat.msg = `Error executing command: ${error.message}`; + // stderr often contains useful info like "service not found" + heartbeat.msg = stderr || stdout || `Service '${serviceName}' is not running.`; reject(new Error(heartbeat.msg)); return; } - const output = stdout.trim(); - - if (monitor.local_service_check_type === "keyword") { - if (monitor.local_service_expected_output) { - if (output.includes(monitor.local_service_expected_output)) { - heartbeat.status = UP; - heartbeat.msg = `OK - Output contains "${monitor.local_service_expected_output}"`; - resolve(); - } else { - heartbeat.status = DOWN; - heartbeat.msg = `Output did not contain "${monitor.local_service_expected_output}"`; - reject(new Error(heartbeat.msg)); - } - } else { - heartbeat.status = UP; - heartbeat.msg = "OK - Command executed successfully"; - resolve(); - } - } else if (monitor.local_service_check_type === "json-query") { - try { - const data = JSON.parse(output); - const { status, response } = await evaluateJsonQuery(data, monitor.jsonPath, monitor.jsonPathOperator, monitor.expectedValue); - - if (status) { - heartbeat.status = UP; - heartbeat.msg = `JSON query passes (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`; - resolve(); - } else { - throw new Error(`JSON query does not pass (comparing ${response} ${monitor.jsonPathOperator} ${monitor.expectedValue})`); - } - } catch (e) { - heartbeat.status = DOWN; - heartbeat.msg = e.message; - reject(e); - } - } + // If there's no error, the service is running. + heartbeat.status = UP; + heartbeat.msg = `Service '${serviceName}' is running.`; + resolve(heartbeat); }); }); } diff --git a/server/server.js b/server/server.js index 104093201..27bf0b388 100644 --- a/server/server.js +++ b/server/server.js @@ -900,9 +900,7 @@ let needSetup = false; bean.rabbitmqPassword = monitor.rabbitmqPassword; bean.conditions = JSON.stringify(monitor.conditions); bean.manual_status = monitor.manual_status; - bean.local_service_command = monitor.local_service_command; - bean.local_service_expected_output = monitor.local_service_expected_output; - bean.local_service_check_type = monitor.local_service_check_type; + bean.local_service_name = monitor.local_service_name; // ping advanced options bean.ping_numeric = monitor.ping_numeric; diff --git a/src/lang/en.json b/src/lang/en.json index a4c745803..d981175c5 100644 --- a/src/lang/en.json +++ b/src/lang/en.json @@ -1025,10 +1025,10 @@ "deleteRemoteBrowserMessage": "Are you sure want to delete this Remote Browser for all monitors?", "GrafanaOncallUrl": "Grafana Oncall URL", "Local Service": "Local Service", + "Service Name": "Service Name", + "localServiceDescription": "The name of the systemd service to check. Example: `nginx.service`", "Browser Screenshot": "Browser Screenshot", "Command": "Command", - "localServiceCommandDescription": "The command to execute. For example: `systemctl is-active mosquitto`", - "localServiceExpectedOutputDescription": "The expected output of the command. If the output contains this string, the monitor will be considered UP. Leave empty to only check the exit code.", "Expected Output": "Expected Output", "mongodbCommandDescription": "Run a MongoDB command against the database. For information about the available commands check out the {documentation}", "wayToGetSevenIOApiKey": "Visit the dashboard under app.seven.io > developer > api key > the green add button", diff --git a/src/pages/EditMonitor.vue b/src/pages/EditMonitor.vue index bf5cb422a..3c1a209f1 100644 --- a/src/pages/EditMonitor.vue +++ b/src/pages/EditMonitor.vue @@ -667,57 +667,10 @@ @@ -1418,9 +1371,7 @@ const monitorDefaults = { rabbitmqUsername: "", rabbitmqPassword: "", conditions: [], - local_service_command: "", - local_service_expected_output: "", - local_service_check_type: "", + local_service_name: "", }; export default {