Simplified and secured Local Service monitor
This commit is contained in:
parent
2ffc06d950
commit
d76ce4e28d
@ -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<void>}
|
||||
*/
|
||||
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");
|
||||
});
|
||||
};
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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<object>} 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -667,57 +667,10 @@
|
||||
|
||||
<template v-if="monitor.type === 'local-service'">
|
||||
<div class="my-3">
|
||||
<label for="local-service-command" class="form-label">{{ $t("Command") }}</label>
|
||||
<input id="local-service-command" v-model="monitor.local_service_command" type="text" class="form-control" required>
|
||||
<label for="local-service-name" class="form-label">{{ $t("Service Name") }}</label>
|
||||
<input id="local-service-name" v-model="monitor.local_service_name" type="text" class="form-control" required placeholder="nginx.service">
|
||||
<div class="form-text">
|
||||
{{ $t("localServiceCommandDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="my-3">
|
||||
<label for="local-service-check-type" class="form-label">{{ $t("Check Type") }}</label>
|
||||
<select id="local-service-check-type" v-model="monitor.local_service_check_type" class="form-select" required>
|
||||
<option value="keyword">{{ $t("Keyword") }}</option>
|
||||
<option value="json-query">{{ $t("Json Query") }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.local_service_check_type === 'keyword'" class="my-3">
|
||||
<label for="local-service-expected-output" class="form-label">{{ $t("Expected Value") }}</label>
|
||||
<input id="local-service-expected-output" v-model="monitor.local_service_expected_output" type="text" class="form-control">
|
||||
<div class="form-text">
|
||||
{{ $t("localServiceExpectedOutputDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="monitor.local_service_check_type === 'json-query'" class="my-3">
|
||||
<div class="my-2">
|
||||
<label for="jsonPath" class="form-label mb-0">{{ $t("Json Query Expression") }}</label>
|
||||
<i18n-t tag="div" class="form-text mb-2" keypath="jsonQueryDescription">
|
||||
<a href="https://jsonata.org/" target="_blank" rel="noopener noreferrer">jsonata.org</a>
|
||||
<a href="https://try.jsonata.org/" target="_blank" rel="noopener noreferrer">{{ $t('playground') }}</a>
|
||||
</i18n-t>
|
||||
<input id="jsonPath" v-model="monitor.jsonPath" type="text" class="form-control" placeholder="$" required>
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-start">
|
||||
<div class="me-2">
|
||||
<label for="json_path_operator" class="form-label">{{ $t("Condition") }}</label>
|
||||
<select id="json_path_operator" v-model="monitor.jsonPathOperator" class="form-select me-3" required>
|
||||
<option value=">">></option>
|
||||
<option value=">=">>=</option>
|
||||
<option value="<"><</option>
|
||||
<option value="<="><=</option>
|
||||
<option value="!=">!=</option>
|
||||
<option value="==">==</option>
|
||||
<option value="contains">contains</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-grow-1">
|
||||
<label for="expectedValue" class="form-label">{{ $t("Expected Value") }}</label>
|
||||
<input v-if="monitor.jsonPathOperator !== 'contains' && monitor.jsonPathOperator !== '==' && monitor.jsonPathOperator !== '!='" id="expectedValue" v-model="monitor.expectedValue" type="number" class="form-control" required step=".01">
|
||||
<input v-else id="expectedValue" v-model="monitor.expectedValue" type="text" class="form-control" required>
|
||||
</div>
|
||||
{{ $t("localServiceDescription") }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -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 {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user