diff --git a/CHANGELOG.md b/CHANGELOG.md index e4afea3..5a14499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,77 @@ --- develop --- +* issue: Drop the device mapping column on uninstall only when it exists; a repeated + uninstall logged a failed ALTER TABLE +* issue: Treat a missing searchFields value as empty in the grid search; a search + string without it raised the PHP 8.1 null deprecation +* issue: Return an empty definition for an unknown Nagios command instead of raising + undefined index and foreach warnings before the not a valid command message +* issue: Log a fixed message when the host import selection is not a JSON object; + the old message converted an array to a string +* issue: Cast the date format, time format and Nagios URL settings to strings in the + NPC frame; unsaved settings raised the PHP 8.1 null deprecation on every page +* dev: Run the schema and the listing, perfdata and upgrade queries against MariaDB + in the Pest suite, and run npc.php, perfdata.php and cli.php as subprocesses + (tests/E2E); the JavaScript front end is tested with Node's test runner +* issue: Write the LIMIT values of the host, service, comment, downtime, event log, + notification and state history listings into the statement as integers; + Cacti binds values as strings and the databases reject a quoted LIMIT, so these + listings failed with an SQL syntax error +* issue: Send the values Acknowledge, Add Comment, Schedule Downtime and Schedule + Check need from the host and service command dialogs; they were rejected for + missing parameters. Sticky, notify and persistent are options in the dialog and + a downtime takes a duration in minutes +* security: Escape quotes in values the front end places inside HTML attributes + (host and service names, user name) +* issue: Show the host, service, comment and event log grids and the summary and + process tables; the front end read a response shape the controllers do not + send, so every table rendered empty +* issue: Document the NPC Global Commands realm that gates remote commands +* issue: Add the minimum_importance column to npc_contacts on upgrade (the upgrade + named a table that does not exist) and remove the settings of deleted users + (the cleanup read a column the query does not return) +* issue: Skip perfdata entries without a numeric value (label=U) in perfdata.php and + cli.php instead of printing PHP warnings into the data input output; both + scripts now share one parser in the base controller +* issue: Declare the hostgroup and unhandled request properties on the base + controller; assigning them raised the PHP 8.2 dynamic property deprecation +* issue: List each service once; the service grid joined the hostgroup members + unconditionally, so a host in several hostgroups repeated its services and + inflated the row count +* issue: Read only the newest host check when fetching host perfdata; the query + returned every stored check and the oldest one was used +* issue: Name the failing class and method in the generic error returned by the + controller logger; the message was built from an invalid property read +* issue: Remove dispatcher actions that cannot work with the single parameter array + they are called with (getSetting, isMapped, mapHost, getSettings, checkHostExists) +* issue: Pass the searchFields and data request values to the controllers as + JSON; the search string sanitizer removed their quotes, so grid searches and the + host import selection never applied; the selection lookup also called a + non-static method statically +* security: Copy only the known request parameters onto the NPC controllers and keep + `start` and `limit` within bounds (at most 1000 rows per request) +* security: Accept the state-changing NPC actions (Nagios commands, comment + deletion, data input creation, host import, graph mapping, settings save) only + as POST requests so Cacti's CSRF token check applies to them +* issue: Load the base controller class in npc.php; every NPC page failed with + a missing Controller class before +* security: Check the NPC command realm before the command file settings so users + without the realm no longer see the configured command file path +* issue: Call the Nagios controller on an instance when deleting all host or service + comments; the static call raised an Error on PHP 8 +* security: Accept only known tab names for the `tab` request variable of the NPC + frame; the raw value was written into an inline script block +* issue: Look up the stored perfdata with the plain object ID when creating a + perfdata data input method; the shell-quoted value never matched, so no output + fields were created +* security: Require the Devices realm for the Nagios host import and the Data Input + Methods realm for creating a perfdata data input method; both were open to any + user with NPC access +* security: Keep the host import address cache as JSON in the Cacti cache directory + instead of a PHP file in the system temp directory that was included on each call +* security: Quote the description, address and template arguments of the Nagios host + import command and reject non-numeric template IDs * dev: Relocate the schema provisioning function `npc_setup_tables()` from `setup.php` into `includes/database.php` (loaded via `require_once`) to align with the fleet schema/includes convention; behavior is unchanged diff --git a/README.md b/README.md index 2c00e0b..a0b1b6d 100644 --- a/README.md +++ b/README.md @@ -124,8 +124,9 @@ Next, there are a few NPC settings to add/update. Goto Console > Configuration > Settings > NPC. * **Remote Commands** - Check the checkbox to enable remote commands. At the - moment this is system wide so all users that can access NPC will be able to - execute commands. + moment this is system wide. A user also needs the "NPC Global Commands" realm + (granted in the user's Realm Permissions) to execute commands; the realm is + checked by name, so no page of that name exists in the plugin directory. * **Nagios Command File Path** - Add the path to the Nagios command file. It will be something like /usr/local/nagios/var/rw/nagios.cmd diff --git a/cli.php b/cli.php index 0ff48ee..469d7d1 100644 --- a/cli.php +++ b/cli.php @@ -137,21 +137,7 @@ function getPerfData() { $obj = new $class; $results = $obj->getPerfData($objectId, $hostname, $serviceName); - $perfParts = explode(' ', $results[0]['perfdata']); - - $output = ''; - - foreach ($perfParts as $perf) { - if (preg_match('/=/', $perf)) { - preg_match('/(\S+)=([-+]?[0-9]*\.?[0-9]+)/', $perf, $matches); - if (preg_match('/^iso./', $matches[1])) { - $matches[1] = 'output'; - } - $output .= $matches[1] . ':' . $matches[2] . ' '; - } - } - - print $output . "\n"; + print $obj->perfdataOutput(isset($results[0]['perfdata']) ? $results[0]['perfdata'] : '') . "\n"; } function listHosts() { diff --git a/controllers/cacti.php b/controllers/cacti.php index 3bdb2a0..3e3dcd4 100644 --- a/controllers/cacti.php +++ b/controllers/cacti.php @@ -76,6 +76,10 @@ function getHostnames() { * @return string json encoded results */ function addDataInputMethod($params) { + if (!api_user_realm_auth('data_input.php')) { + return(json_encode(array('success' => false, 'msg' => __('You do not have permission to create data input methods.', 'npc')))); + } + $name = 'NPC - Perfdata - ' . $params['host']; if (isset($params['service'])) { @@ -87,9 +91,9 @@ function addDataInputMethod($params) { $class = 'NpcHostsController'; } - $object_id = cacti_escapeshellarg($params['object_id']); + $object_id = $params['object_id']; - $input = "php /plugins/npc/perfdata.php --type=$type --id=$object_id 2> /dev/null"; + $input = "php /plugins/npc/perfdata.php --type=$type --id=" . cacti_escapeshellarg($object_id) . " 2> /dev/null"; $save = array(); $save['hash'] = $this->generateHash(); @@ -102,7 +106,7 @@ function addDataInputMethod($params) { $obj = new $class; $results = $obj->getPerfData($object_id); - $perfParts = explode(';', $results[0]['perfdata']); + $perfParts = explode(';', isset($results[0]['perfdata']) ? $results[0]['perfdata'] : ''); foreach ($perfParts as $perf) { if (preg_match('/=/', $perf)) { diff --git a/controllers/comments.php b/controllers/comments.php index aca79ae..7f5543b 100644 --- a/controllers/comments.php +++ b/controllers/comments.php @@ -118,6 +118,7 @@ function deleteAllHostComments() { require_once("plugins/npc/controllers/nagios.php"); $seen = array(); + $nagios = new NpcNagiosController(); $results = $this->flattenArray($this->comments(null, 'o.objecttype_id = 1')); @@ -128,7 +129,7 @@ function deleteAllHostComments() { 'command' => 'DEL_ALL_HOST_COMMENTS', 'host_name' => $host ); - NpcNagiosController::command($params); + $nagios->command($params); $seen[$host] = 1; } } @@ -146,6 +147,7 @@ function deleteAllServiceComments() { require_once("plugins/npc/controllers/nagios.php"); $seen = array(); + $nagios = new NpcNagiosController(); $results = $this->flattenArray($this->comments(null, 'o.objecttype_id = 2')); @@ -158,7 +160,7 @@ function deleteAllServiceComments() { 'host_name' => $host, 'service_description' => $service, ); - NpcNagiosController::command($params); + $nagios->command($params); $seen[$host][$service] = 1; } } @@ -241,7 +243,6 @@ function comments($id = null, $where = '') { ' . $whereClause, $params); - $offset = ($this->currentPage - 1) * $this->limit; $results = db_fetch_assoc_prepared('SELECT i.instance_name, o.name1 AS host_name, @@ -258,8 +259,8 @@ function comments($id = null, $where = '') { LEFT JOIN npc_hosts h ON c.object_id = h.host_object_id ' . $whereClause . ' ORDER BY ' . $orderBy . ' - LIMIT ?, ?', - array_merge($params, array($offset, $this->limit))); + ' . $this->limitClause(), + $params); return($results); } diff --git a/controllers/controller.php b/controllers/controller.php index 876703c..4ec0de5 100644 --- a/controllers/controller.php +++ b/controllers/controller.php @@ -120,6 +120,22 @@ class Controller { */ var $searchString = null; + /** + * Restricts a listing to the hostgroup with this alias + * + * @var string + * @access public + */ + var $hostgroup = null; + + /** + * When set, a service listing leaves out acknowledged problems + * + * @var string + * @access public + */ + var $unhandled = null; + /** * json encoded list of fields to search * @@ -253,6 +269,48 @@ function __construct() { $this->config_type = isset($config_type) ? $config_type : 1; } + /** + * limitClause + * + * The LIMIT clause for the current page. The values are written into the + * statement because Cacti's database layer sends bound values as strings, + * and MariaDB and MySQL reject a quoted LIMIT value. + * + * @return string for example " LIMIT 0, 25" + */ + function limitClause() { + $limit = max(1, (int) $this->limit); + $offset = max(0, ((int) $this->currentPage - 1) * $limit); + + return ' LIMIT ' . $offset . ', ' . $limit; + } + + /** + * perfdataOutput + * + * Reduces Nagios perfdata to the "label:value" pairs a Cacti data input + * method reads. Entries without a numeric value, such as label=U, are left + * out. + * + * @param string $perfdata The perfdata of a check + * @return string for example "load1:0.52 load5:0.4 " + */ + function perfdataOutput($perfdata) { + $output = ''; + + foreach (explode(' ', (string) $perfdata) as $perf) { + if (preg_match('/(\S+)=([-+]?[0-9]*\.?[0-9]+)/', $perf, $matches)) { + if (preg_match('/^iso./', $matches[1])) { + $matches[1] = 'output'; + } + + $output .= $matches[1] . ':' . $matches[2] . ' '; + } + } + + return $output; + } + function jsonOutput($results=array()) { if (!$this->numRecords) { $this->numRecords = count($results); @@ -322,7 +380,7 @@ function flattenArray($array=array()) { * @return string The appended where clause */ function searchClause($where, $fieldMap, &$params = array()) { - $fields = json_decode(stripslashes($this->searchFields)); + $fields = json_decode(stripslashes((string) $this->searchFields)); if (!is_array($fields)) { return $where; } @@ -416,7 +474,7 @@ function logger($level, $class, $method, $message) { // If this was an error send a generic response to the client if ($level == 'error') { - return(json_encode(array('success' => false, 'msg' => __('An error occurred in %s. See error logs for detail.', $class -> $method, 'npc')))); + return(json_encode(array('success' => false, 'msg' => sprintf(__('An error occurred in %s. See error logs for detail.', 'npc'), $class . '::' . $method)))); } } diff --git a/controllers/downtime.php b/controllers/downtime.php index 6584d44..db859f8 100644 --- a/controllers/downtime.php +++ b/controllers/downtime.php @@ -181,7 +181,6 @@ function downtimeHistory($id = null, $where = '') { ' . $whereClause, $params); - $offset = ($this->currentPage - 1) * $this->limit; $results = db_fetch_assoc_prepared('SELECT i.instance_name, o.name1 AS host_name, @@ -192,8 +191,8 @@ function downtimeHistory($id = null, $where = '') { LEFT JOIN npc_instances i ON d.instance_id = i.instance_id ' . $whereClause . ' ORDER BY d.scheduled_start_time DESC, d.actual_start_time DESC, d.actual_start_time_usec DESC - LIMIT ?, ?', - array_merge($params, array($offset, $this->limit))); + ' . $this->limitClause(), + $params); return($results); } diff --git a/controllers/hosts.php b/controllers/hosts.php index f6e79bf..5c1b1c1 100644 --- a/controllers/hosts.php +++ b/controllers/hosts.php @@ -103,7 +103,9 @@ function summary() { function getPerfData($id) { return db_fetch_assoc_prepared('SELECT perfdata FROM npc_hostchecks - WHERE host_object_id = ?', + WHERE host_object_id = ? + ORDER BY hostcheck_id DESC + LIMIT 1', array($id)); } @@ -156,7 +158,6 @@ function hosts() { $params); /* Paginated results */ - $offset = ($this->currentPage - 1) * $this->limit; $hosts = db_fetch_assoc_prepared( 'SELECT i.instance_name, @@ -177,8 +178,8 @@ function hosts() { LEFT JOIN npc_host_graphs hg ON hs.host_object_id = hg.host_object_id WHERE ' . $where . ' ORDER BY ' . $orderBy . ' - LIMIT ?, ?', - array_merge($params, array($offset, $this->limit))); + ' . $this->limitClause(), + $params); return $hosts; } diff --git a/controllers/layout.php b/controllers/layout.php index 3618c31..e354550 100644 --- a/controllers/layout.php +++ b/controllers/layout.php @@ -10,11 +10,6 @@ function drawFrame($params) { $npc_base = $config['url_path'] . 'plugins/npc/'; - $current_tab = 'dashboard'; - if (isset_request_var('tab')) { - $current_tab = get_request_var('tab'); - } - $tabs = array( 'dashboard' => __('Dashboard', 'npc'), 'hosts' => __('Hosts', 'npc'), @@ -25,6 +20,16 @@ function drawFrame($params) { 'process' => __('Process Info', 'npc'), ); + /* only a known tab name is ever written into the page */ + $current_tab = 'dashboard'; + if (isset_request_var('tab')) { + $requested_tab = get_request_var('tab'); + + if (is_string($requested_tab) && isset($tabs[$requested_tab])) { + $current_tab = $requested_tab; + } + } + /* draw the tabs */ print "