tags, safely across * Cacti versions. Newer Cacti releases enforce a Content-Security-Policy that * requires a per-request nonce on parser-inserted scripts; older releases lack * the CactiSecureHeaders class, so this returns an empty string there. * * @return string The nonce attribute when supported, otherwise empty string. */ function plugin_monitor_csp_nonce(): string { if (class_exists('CactiSecureHeaders')) { return CactiSecureHeaders::getNonceAttribute(); } return ''; } /** * Plugin install hook: registers all of this plugin's Cacti hooks * (header tabs, navigation text, config form/settings/arrays, * poller_bottom, page_head, device save/action/remove integration, and * device list filter/SQL/table hooks), registers its viewer realm, sets * default settings, and creates its database tables. Called by Cacti's * plugin architecture when the plugin is installed. * * @return void */ function plugin_monitor_install() { global $config; require_once($config['base_path'] . '/plugins/monitor/includes/database.php'); // core plugin functionality api_plugin_register_hook('monitor', 'top_header_tabs', 'monitor_show_tab', 'setup.php'); api_plugin_register_hook('monitor', 'top_graph_header_tabs', 'monitor_show_tab', 'setup.php'); api_plugin_register_hook('monitor', 'top_graph_refresh', 'monitor_top_graph_refresh', 'setup.php'); api_plugin_register_hook('monitor', 'draw_navigation_text', 'monitor_draw_navigation_text', 'setup.php'); api_plugin_register_hook('monitor', 'config_form', 'monitor_config_form', 'setup.php'); api_plugin_register_hook('monitor', 'config_settings', 'monitor_config_settings', 'setup.php'); api_plugin_register_hook('monitor', 'config_arrays', 'monitor_config_arrays', 'setup.php'); api_plugin_register_hook('monitor', 'poller_bottom', 'monitor_poller_bottom', 'setup.php'); api_plugin_register_hook('monitor', 'page_head', 'plugin_monitor_page_head', 'setup.php'); // device actions and interaction api_plugin_register_hook('monitor', 'api_device_save', 'monitor_api_device_save', 'setup.php'); api_plugin_register_hook('monitor', 'device_action_array', 'monitor_device_action_array', 'setup.php'); api_plugin_register_hook('monitor', 'device_action_execute', 'monitor_device_action_execute', 'setup.php'); api_plugin_register_hook('monitor', 'device_action_prepare', 'monitor_device_action_prepare', 'setup.php'); api_plugin_register_hook('monitor', 'device_remove', 'monitor_device_remove', 'setup.php'); // add new filter for device api_plugin_register_hook('monitor', 'device_filters', 'monitor_device_filters', 'setup.php'); api_plugin_register_hook('monitor', 'device_sql_where', 'monitor_device_sql_where', 'setup.php'); api_plugin_register_hook('monitor', 'device_table_bottom', 'monitor_device_table_bottom', 'setup.php'); api_plugin_register_realm('monitor', 'monitor.php', 'View Monitoring Dashboard', 1); set_config_option('monitor_view', 'default'); set_config_option('monitor_grouping', 'default'); set_config_option('monitor_trim', '4000'); set_config_option('monitor_rows', 100); monitor_setup_table(); } /** * Device_filters hook: adds a 'Criticality' filter option to Cacti's * device list filter form. Called by Cacti's host list page via the * 'device_filters' hook. * * @param array $filters The device list's filter definitions array * being built up. * * @return array The $filters array with the criticality filter added. */ function monitor_device_filters($filters) { $criticalities = [ '-1' => __('Any', 'monitor'), '0' => __('None', 'monitor'), '1' => __('Low', 'monitor'), '2' => __('Medium', 'monitor'), '3' => __('High', 'monitor'), '4' => __('Mission Critical', 'monitor') ]; $filters['criticality'] = [ 'friendly_name' => __('Criticality', 'monitor'), 'method' => 'drop_array', 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', 'array' => $criticalities, 'value' => '-1' ]; return $filters; } /** * Device_sql_where hook: appends a criticality condition to the device * list's SQL WHERE clause when a specific criticality filter is * selected. Called by Cacti's host list page via the * 'device_sql_where' hook. * * @param string $sql_where The SQL WHERE clause being built up. * * @return string The $sql_where string with the criticality condition * appended, if applicable. */ function monitor_device_sql_where($sql_where) { if (get_request_var('criticality') >= 0) { $sql_where .= ($sql_where != '' ? ' AND ' : 'WHERE ') . ' monitor_criticality = ' . get_request_var('criticality'); } return $sql_where; } /** * Device_table_bottom hook: on Cacti versions prior to 1.3.0 (which * lack native support for plugin-added filter fields), injects the * criticality filter's <select> element into the device list's * filter row via client-side JS and wires up its change handler to * reapply the page filter. On 1.3.0+, this is a no-op since the * 'device_filters' hook handles rendering natively. Called by Cacti's * host list page via the 'device_table_bottom' hook. * * @return void */ function monitor_device_table_bottom() { $criticalities = [ '-1' => __('Any', 'monitor'), '0' => __('None', 'monitor'), '1' => __('Low', 'monitor'), '2' => __('Medium', 'monitor'), '3' => __('High', 'monitor'), '4' => __('Mission Critical', 'monitor') ]; if (version_compare(CACTI_VERSION, '1.3.0', '<')) { $select = '
" . __('Click \'Continue\' to %s monitoring on these Device(s)', $action_description, 'monitor') . "
" . __('Click \'Continue\' to Change the Monitoring settings for the following Device(s). Remember to check \'Update this Field\' to indicate which columns to update.', 'monitor') . "
The following Device\'s were Rebooted. See details below for additional information.
';
} else {
print '
';
}
}
}
/**
* Config_form hook: injects this plugin's monitoring settings fields
* (enable checkbox, criticality, ping warn/alert thresholds and their
* baseline-rebase drop-downs, down-device message, device icon) into
* the host edit form, right after the bulk-walk-size field (or
* 'disabled' as a fallback anchor point). Called by Cacti's host edit
* page via the 'config_form' hook.
*
* @return void
*
* @global array $config Reserved/declared for parity with
* other functions in this file; not
* used directly here.
* @global array $fields_host_edit The host edit form's field
* definitions array; replaced here with
* a version containing this plugin's
* injected fields.
* @global array $criticalities Map of criticality level => display
* label, used for the criticality
* drop-down.
* @global array $fa_icons Map of icon key => icon definition,
* used for the device icon selector.
*/
function monitor_config_form() {
global $config, $fields_host_edit, $criticalities, $fa_icons;
$baselines = [
'0' => __('Do not Change', 'monitor'),
'1.20' => __('%d Percent Above Average', 20, 'monitor'),
'1.30' => __('%d Percent Above Average', 30, 'monitor'),
'1.40' => __('%d Percent Above Average', 40, 'monitor'),
'1.50' => __('%d Percent Above Average', 50, 'monitor'),
'1.60' => __('%d Percent Above Average', 60, 'monitor'),
'1.70' => __('%d Percent Above Average', 70, 'monitor'),
'1.80' => __('%d Percent Above Average', 80, 'monitor'),
'1.90' => __('%d Percent Above Average', 90, 'monitor'),
'2.00' => __('%d Percent Above Average', 100, 'monitor'),
'2.20' => __('%d Percent Above Average', 120, 'monitor'),
'2.40' => __('%d Percent Above Average', 140, 'monitor'),
'2.50' => __('%d Percent Above Average', 150, 'monitor'),
'3.00' => __('%d Percent Above Average', 200, 'monitor'),
'4.00' => __('%d Percent Above Average', 300, 'monitor'),
'5.00' => __('%d Percent Above Average', 400, 'monitor'),
'6.00' => __('%d Percent Above Average', 500, 'monitor')
];
$fields_host_edit2 = $fields_host_edit;
$fields_host_edit3 = [];
if (array_key_exists('bulk_walk_size', $fields_host_edit2)) {
$insert_field = 'bulk_walk_size';
} else {
$insert_field = 'disabled';
}
foreach ($fields_host_edit2 as $f => $a) {
$fields_host_edit3[$f] = $a;
if ($f == $insert_field) {
$fields_host_edit3['monitor_header'] = [
'friendly_name' => __('Device Monitoring Settings', 'monitor'),
'method' => 'spacer',
'collapsible' => 'true'
];
$fields_host_edit3['monitor'] = [
'method' => 'checkbox',
'friendly_name' => __('Monitor Device', 'monitor'),
'description' => __('Check this box to monitor this Device on the Monitor Tab.', 'monitor'),
'value' => '|arg1:monitor|',
'form_id' => false
];
$host_id = get_nfilter_request_var('id');
if (empty($host_id) || !is_numeric($host_id)) {
$fields_host_edit3['monitor']['default'] = monitor_get_default($host_id);
}
$fields_host_edit3['monitor_criticality'] = [
'friendly_name' => __('Device Criticality', 'monitor'),
'description' => __('What is the Criticality of this Device.', 'monitor'),
'method' => 'drop_array',
'array' => $criticalities,
'value' => '|arg1:monitor_criticality|',
'default' => '0',
];
$fields_host_edit3['monitor_warn'] = [
'friendly_name' => __('Ping Warning Threshold', 'monitor'),
'description' => __('If the round-trip latency via any of the predefined Cacti ping methods raises above this threshold, log a warning or send email based upon the Devices Criticality and Monitor setting. The unit is in milliseconds. Setting to 0 disables. The Thold Plugin is required to leverage this functionality.', 'monitor'),
'method' => 'textbox',
'size' => '10',
'max_length' => '5',
'placeholder' => __('milliseconds', 'monitor'),
'value' => '|arg1:monitor_warn|',
'default' => '',
];
$fields_host_edit3['monitor_alert'] = [
'friendly_name' => __('Ping Alert Threshold', 'monitor'),
'description' => __('If the round-trip latency via any of the predefined Cacti ping methods raises above this threshold, log an alert or send an email based upon the Devices Criticality and Monitor setting. The unit is in milliseconds. Setting to 0 disables. The Thold Plugin is required to leverage this functionality.', 'monitor'),
'method' => 'textbox',
'size' => '10',
'max_length' => '5',
'placeholder' => __('milliseconds', 'monitor'),
'value' => '|arg1:monitor_alert|',
'default' => '',
];
$fields_host_edit3['monitor_warn_baseline'] = [
'friendly_name' => __('Re-Baseline Warning', 'monitor'),
'description' => __('The percentage above the current average ping time to consider a Warning Threshold. If updated, this will automatically adjust the Ping Warning Threshold.', 'monitor'),
'method' => 'drop_array',
'default' => '0',
'value' => '0',
'array' => $baselines
];
$fields_host_edit3['monitor_alert_baseline'] = [
'friendly_name' => __('Re-Baseline Alert', 'monitor'),
'description' => __('The percentage above the current average ping time to consider a Alert Threshold. If updated, this will automatically adjust the Ping Alert Threshold.', 'monitor'),
'method' => 'drop_array',
'default' => '0',
'value' => '0',
'array' => $baselines
];
$fields_host_edit3['monitor_text'] = [
'friendly_name' => __('Down Device Message', 'monitor'),
'description' => __('This is the message that will be displayed when this Device is reported as down.', 'monitor'),
'method' => 'textarea',
'max_length' => 1000,
'textarea_rows' => 2,
'textarea_cols' => 80,
'value' => '|arg1:monitor_text|',
'default' => '',
];
if (function_exists('form_dropicon')) {
$method = 'drop_icon';
} else {
$method = 'drop_array';
}
$fields_host_edit3['monitor_icon'] = [
'friendly_name' => __('Device icon', 'monitor'),
'description' => __('You can select device icon.', 'monitor'),
'method' => $method,
'default' => '0',
'value' => '|arg1:monitor_icon|',
'array' => $fa_icons,
];
}
}
$fields_host_edit = $fields_host_edit3;
}
/**
* Determines the default 'Monitor Device' checkbox state for a new
* (not-yet-saved) host, based on the globally configured
* 'monitor_new_enabled' setting. Called from monitor_config_form() when
* building the host edit form for a new device.
*
* @param int|string $host_id The host id being edited; only new (empty
* or non-numeric) hosts get the configured
* default.
*
* @return string The default value ('' or 'on') for the monitor
* checkbox.
*/
function monitor_get_default($host_id) {
$monitor_new_device = '';
if ($host_id <= 0) {
$monitor_new_device = read_config_option('monitor_new_enabled');
}
return $monitor_new_device;
}
/**
* Api_device_save hook: validates and persists this plugin's submitted
* monitoring fields (enable flag, down-device message, criticality,
* ping warn/alert thresholds, icon) onto the host save data,
* recomputing the warn/alert thresholds from the device's current ping
* time when a baseline-rebase percentage was submitted. Called by
* Cacti's host save flow via the 'api_device_save' hook.
*
* @param array $save The host's save data array being built up.
*
* @return array The $save array with this plugin's monitoring fields
* added/validated.
*
* @global array $fa_icons Map of valid icon key => icon definition,
* used to validate the submitted icon
* selection.
*/
function monitor_api_device_save($save) {
global $fa_icons;
$monitor_default = monitor_get_default($save['id']);
if (isset_request_var('monitor')) {
$save['monitor'] = form_input_validate(get_nfilter_request_var('monitor'), 'monitor', $monitor_default, true, 3);
} else {
$save['monitor'] = form_input_validate($monitor_default, 'monitor', '', true, 3);
}
if (isset_request_var('monitor_text')) {
$save['monitor_text'] = form_input_validate(get_nfilter_request_var('monitor_text'), 'monitor_text', '', true, 3);
} else {
$save['monitor_text'] = form_input_validate('', 'monitor_text', '', true, 3);
}
if (isset_request_var('monitor_criticality')) {
$save['monitor_criticality'] = form_input_validate(get_nfilter_request_var('monitor_criticality'), 'monitor_criticality', '^[0-9]+$', true, 3);
} else {
$save['monitor_criticality'] = form_input_validate('', 'monitor_criticality', '', true, 3);
}
if (isset_request_var('monitor_warn')) {
$save['monitor_warn'] = form_input_validate(get_nfilter_request_var('monitor_warn'), 'monitor_warn', '^[0-9]+$', true, 3);
} else {
$save['monitor_warn'] = form_input_validate('', 'monitor_warn', '', true, 3);
}
if (isset_request_var('monitor_alert')) {
$save['monitor_alert'] = form_input_validate(get_nfilter_request_var('monitor_alert'), 'monitor_alert', '^[0-9]+$', true, 3);
} else {
$save['monitor_alert'] = form_input_validate('', 'monitor_alert', '', true, 3);
}
if (isset_request_var('monitor_icon') && array_key_exists(get_nfilter_request_var('monitor_icon'), $fa_icons)) {
$save['monitor_icon'] = get_nfilter_request_var('monitor_icon');
} else {
$save['monitor_icon'] = '';
}
if (!isempty_request_var('monitor_alert_baseline') && !empty($save['id'])) {
$cur_time = db_fetch_cell_prepared('SELECT cur_time
FROM host
WHERE id = ?',
[$save['id']]);
if ($cur_time > 0) {
$save['monitor_alert'] = ceil($cur_time * get_nfilter_request_var('monitor_alert_baseline'));
}
}
if (!isempty_request_var('monitor_warn_baseline') && !empty($save['id'])) {
$cur_time = db_fetch_cell_prepared('SELECT cur_time
FROM host
WHERE id = ?',
[$save['id']]);
if ($cur_time > 0) {
$save['monitor_warn'] = ceil($cur_time * get_nfilter_request_var('monitor_alert_baseline'));
}
}
return $save;
}
/**
* Draw_navigation_text hook: registers the breadcrumb/navigation title
* entry for this plugin's monitor.php page. Called by Cacti's
* navigation framework via the 'draw_navigation_text' hook.
*
* @param array $nav The navigation entries array being built up.
*
* @return array The $nav array with this plugin's entry added.
*/
function monitor_draw_navigation_text($nav) {
$nav['monitor.php:'] = ['title' => __('Monitoring', 'monitor'), 'mapping' => '', 'url' => 'monitor.php', 'level' => '0'];
return $nav;
}
/**
* Poller_bottom hook: on the main poller (poller_id 1), launches this
* plugin's poller_monitor.php script as a background process at the
* end of each Cacti polling cycle. Called by Cacti's poller via the
* 'poller_bottom' hook.
*
* @return void
*
* @global array $config Cacti global configuration array; used to
* check the poller id and locate the PHP binary
* and this plugin's poller script.
*/
function monitor_poller_bottom() {
global $config;
if ($config['poller_id'] == 1) {
require_once($config['library_path'] . '/poller.php');
$command_string = trim(read_config_option('path_php_binary'));
if (trim($command_string) == '') {
$command_string = 'php';
}
$extra_args = ' -q ' . $config['base_path'] . '/plugins/monitor/poller_monitor.php';
exec_background($command_string, $extra_args);
}
}
/**
* Removes files and directories that a previous version of this plugin
* shipped but that have since moved or been deleted, using the tombstone
* and whitelist lists in manifest.json. Whitelisted (user-data) paths and
* any VCS metadata (.git*) are never touched; the dev-only tests/ tree is
* removed. Any path that resolves outside the plugin directory (a tampered
* manifest.json) is refused, and any file/directory that cannot be removed
* (e.g. read-only) is reported to the Cacti log. Any top-level entry that is
* neither expected nor a tombstone nor whitelisted is logged to the Cacti
* log and left in place. Called on a plugin version change.
*
* @return void
*
* @global array $config Cacti global configuration array; used to resolve
* the plugin directory.
*/
function monitor_prune_files(): void {
global $config;
$plugin_dir = $config['base_path'] . '/plugins/monitor';
$manifest_path = $plugin_dir . '/manifest.json';
if (!is_readable($manifest_path)) {
return;
}
$manifest = json_decode((string) file_get_contents($manifest_path), true);
if (!is_array($manifest)) {
cacti_log('WARNING: monitor manifest.json could not be parsed; skipping file prune', false, 'MONITOR');
return;
}
$tombstones = isset($manifest['tombstones']) && is_array($manifest['tombstones']) ? $manifest['tombstones'] : [];
$expected = isset($manifest['expected']) && is_array($manifest['expected']) ? $manifest['expected'] : [];
$whitelist = isset($manifest['whitelist']) && is_array($manifest['whitelist']) ? $manifest['whitelist'] : [];
$protected = function (string $rel) use ($whitelist): bool {
if (strncmp($rel, '.git', 4) === 0 || strncmp($rel, '.md', 3) === 0) {
return true;
}
foreach ($whitelist as $entry) {
$entry = trim((string) $entry, '/');
if ($entry !== '' && ($rel === $entry
|| strncmp($rel, $entry . '/', strlen($entry) + 1) === 0
|| strncmp($entry, $rel . '/', strlen($rel) + 1) === 0)) {
return true;
}
}
return false;
};
// Security: resolve the plugin directory so a tampered manifest.json
// cannot steer the prune outside of it.
$plugin_real = realpath($plugin_dir);
// Remove tombstoned (moved/deleted) paths plus the dev-only tests/
// tree and the phpunit.xml test configuration.
$remove = $tombstones;
$remove[] = 'tests/';
$remove[] = 'phpunit.xml';
foreach ($remove as $rel) {
$rel = trim((string) $rel, '/');
if ($rel === '' || $protected($rel)) {
continue;
}
// A tombstone must never contain '.'/'..' segments; a tampered manifest
// could use them to escape the plugin directory or target its root.
$segments = explode('/', $rel);
if (in_array('.', $segments, true) || in_array('..', $segments, true)) {
cacti_log(sprintf('WARNING: monitor prune refused to remove %s: path contains a traversal segment (tampered manifest.json?)', $rel), false, 'MONITOR');
continue;
}
$path = $plugin_dir . '/' . $rel;
if (!is_link($path) && !file_exists($path)) {
continue;
}
// Refuse any path that, after resolving symlinks and ../ segments,
// escapes the plugin directory (protects user data from a tampered
// manifest.json).
$anchor = is_link($path) ? dirname($path) : $path;
$real = realpath($anchor);
if ($real === false || ($real !== $plugin_real && strncmp($real, $plugin_real . DIRECTORY_SEPARATOR, strlen((string) $plugin_real) + 1) !== 0)) {
cacti_log(sprintf('WARNING: monitor prune refused to remove %s: path resolves outside the plugin directory (tampered manifest.json?)', $rel), false, 'MONITOR');
continue;
}
if (is_dir($path) && !is_link($path)) {
$removed = monitor_rmtree($path);
} else {
$removed = @unlink($path);
}
if (!$removed) {
cacti_log(sprintf('WARNING: monitor upgrade could not remove %s (check file/directory permissions)', $rel), false, 'MONITOR');
}
}
// Surface any top-level entry the manifest does not account for.
$known = [];
foreach (array_merge($expected, $tombstones) as $entry) {
$top = explode('/', trim((string) $entry, '/'))[0];
if ($top !== '') {
$known[$top] = true;
}
}
$entries = scandir($plugin_dir);
foreach (($entries !== false ? $entries : []) as $entry) {
if ($entry === '.' || $entry === '..' || $entry === 'tests' || $entry === 'phpunit.xml' || $protected($entry) || isset($known[$entry])) {
continue;
}
cacti_log(sprintf('WARNING: monitor upgrade found a file/directory not described in manifest.json: %s (left in place)', $entry), false, 'MONITOR');
}
}
/**
* Recursively deletes a directory and its contents. Symlinks are removed
* without being followed. Helper for monitor_prune_files().
*
* @param string $dir Absolute path to the directory to remove.
*
* @return bool True if the directory and everything under it was removed;
* false if any entry could not be deleted.
*/
function monitor_rmtree(string $dir): bool {
$entries = scandir($dir);
$ok = true;
foreach (($entries !== false ? $entries : []) as $entry) {
if ($entry === '.' || $entry === '..') {
continue;
}
$path = $dir . '/' . $entry;
if (is_dir($path) && !is_link($path)) {
if (!monitor_rmtree($path)) {
$ok = false;
}
} elseif (!@unlink($path)) {
$ok = false;
}
}
if (!@rmdir($dir)) {
$ok = false;
}
return $ok;
}