diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1e7f2..da153e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ --- develop --- +* issue#278: Extract alert command execution into shared helper in functions.php; command tokenization now uses preg_split (handles tabs and consecutive spaces); /bin/sh fallback for non-executable command templates removed (use absolute paths with execute bit set) * issue: Making changes to support Cacti 1.3 * issue: Don't use MyISAM for non-analytical tables * issue: The install advisor for Syslog was broken in current Cacti releases diff --git a/functions.php b/functions.php index 9bd3223..af2fc42 100644 --- a/functions.php +++ b/functions.php @@ -1081,6 +1081,93 @@ function syslog_array2xml($array, $tag = 'template') { return $xml; } +/** + * syslog_execute_ticket_command - run the configured ticketing command for an alert + * + * @param array $alert The alert row from syslog_alert table + * @param array $hostlist Hostnames matched by the alert + * @param string $context Short label for the log entry (e.g. 'Ticket Command', 'Command') + * + * @return void + */ +function syslog_execute_ticket_command($alert, $hostlist, $context) { + $command = read_config_option('syslog_ticket_command'); + + if ($command != '') { + $command = trim($command); + } + + if ($alert['open_ticket'] == 'on' && $command != '') { + /* trim surrounding quotes so paths like "/usr/bin/cmd" resolve correctly */ + $cparts = preg_split('/\s+/', trim($command)); + $executable = trim($cparts[0], '"\''); + + if (is_executable($executable)) { + $command = $command . + ' --alert-name=' . cacti_escapeshellarg(clean_up_name($alert['name'])) . + ' --severity=' . cacti_escapeshellarg($alert['severity']) . + ' --hostlist=' . cacti_escapeshellarg(implode(',', $hostlist)) . + ' --message=' . cacti_escapeshellarg($alert['message']); + + $output = array(); + $return = 0; + + exec($command, $output, $return); + + if ($return !== 0) { + cacti_log(sprintf('ERROR: %s Failed. Alert:%s, Exit:%s, Output:%s', $context, $alert['name'], $return, implode(', ', $output)), false, 'SYSLOG'); + } + } else { + $reason = (strpos($executable, DIRECTORY_SEPARATOR) === false) + ? 'PATH-based lookups are not supported; use an absolute path' + : 'file not found or not marked executable'; + cacti_log("SYSLOG ERROR: $context is not executable: '$command' -- $reason", false, 'SYSTEM'); + } + } +} + +/** + * syslog_execute_alert_command - run the per-alert shell command for a matched result + * + * @param array $alert The alert row from syslog_alert table + * @param array $results The matched syslog result row + * @param string $hostname Resolved hostname for the source device + * + * @return void + */ +function syslog_execute_alert_command($alert, $results, $hostname) { + /* alert_replace_variables() escapes each substituted token (, + * , , , , ) with + * cacti_escapeshellarg(). The command template itself comes from admin + * configuration ($alert['command']) and is trusted at that boundary. + * Do not introduce additional substitution paths that bypass this escaping. */ + $command = alert_replace_variables($alert, $results, $hostname); + + /* trim surrounding quotes so paths like "/usr/bin/cmd" resolve correctly */ + $cparts = preg_split('/\s+/', trim($command)); + $executable = trim($cparts[0], '"\''); + + $output = array(); + $return = 0; + + if (is_executable($executable)) { + exec($command, $output, $return); + + if ($return !== 0 && !empty($output)) { + cacti_log('SYSLOG NOTICE: Alert command output: ' . implode(', ', $output), true, 'SYSTEM'); + } + + if ($return !== 0) { + cacti_log(sprintf('ERROR: Alert command failed. Alert:%s, Exit:%s, Output:%s', $alert['name'], $return, implode(', ', $output)), false, 'SYSLOG'); + } + } else { + $reason = (strpos($executable, DIRECTORY_SEPARATOR) === false) + ? 'PATH-based lookups are not supported; use an absolute path' + : 'file not found or not marked executable'; + cacti_log("SYSLOG ERROR: Alert command is not executable: '$command' -- $reason", false, 'SYSTEM'); + } +} + /** * syslog_process_alerts - Process each of the Syslog Alerts * @@ -1495,49 +1582,10 @@ function syslog_process_alert($alert, $sql, $params, $count, $hostname = '') { /** * Open a ticket if this options have been selected. */ - $command = read_config_option('syslog_ticket_command'); - - if ($command != '') { - $command = trim($command); - } - - if ($alert['open_ticket'] == 'on' && $command != '') { - if (is_executable($command)) { - $command = $command . - ' --alert-name=' . cacti_escapeshellarg(clean_up_name($alert['name'])) . - ' --severity=' . cacti_escapeshellarg($alert['severity']) . - ' --hostlist=' . cacti_escapeshellarg(implode(',',$hostlist)) . - ' --message=' . cacti_escapeshellarg($alert['message']); - - $output = array(); - $return = 0; - - exec($command, $output, $return); - - if ($return != 0) { - cacti_log(sprintf('ERROR: Ticket Command Failed. Alert:%s, Exit:%s, Output:%s', $alert['name'], $return, implode(', ', $output)), false, 'SYSLOG'); - } - } - } + syslog_execute_ticket_command($alert, $hostlist, 'Ticket Command'); if (trim($alert['command']) != '' && !$found) { - $command = alert_replace_variables($alert, $results, $hostname); - - $logMessage = "SYSLOG NOTICE: Executing '$command'"; - - $cparts = explode(' ', $command); - - if (is_executable($cparts[0])) { - exec($command, $output, $returnCode); - } else { - exec('/bin/sh ' . $command, $output, $returnCode); - } - - // Append the return code to the log message without the dot - $logMessage .= " Command return code: $returnCode"; - - // Log the combined message - cacti_log($logMessage, true, 'SYSTEM'); + syslog_execute_alert_command($alert, $results, $hostname); } } @@ -1555,49 +1603,10 @@ function syslog_process_alert($alert, $sql, $params, $count, $hostname = '') { alert_setup_environment($alert, $results, $hostlist, $hostname); - $command = read_config_option('syslog_ticket_command'); - - if ($command != '') { - $command = trim($command); - } - - if ($alert['open_ticket'] == 'on' && $command != '') { - if (is_executable($command)) { - $command = $command . - ' --alert-name=' . cacti_escapeshellarg(clean_up_name($alert['name'])) . - ' --severity=' . cacti_escapeshellarg($alert['severity']) . - ' --hostlist=' . cacti_escapeshellarg(implode(',',$hostlist)) . - ' --message=' . cacti_escapeshellarg($alert['message']); - - $output = array(); - $return = 0; - - exec($command, $output, $return); - - if ($return != 0) { - cacti_log(sprintf('ERROR: Command Failed. Alert:%s, Exit:%s, Output:%s', $alert['name'], $return, implode(', ', $output)), false, 'SYSLOG'); - } - } - } + syslog_execute_ticket_command($alert, $hostlist, 'Command'); if (trim($alert['command']) != '' && !$found) { - $command = alert_replace_variables($alert, $results, $hostname); - - $logMessage = "SYSLOG NOTICE: Executing '$command'"; - - $cparts = explode(' ', $command); - - if (is_executable($cparts[0])) { - exec($command, $output, $returnCode); - } else { - exec('/bin/sh ' . $command, $output, $returnCode); - } - - // Append the return code to the log message without the dot - $logMessage .= " Command return code: $returnCode"; - - // Log the combined message - cacti_log($logMessage, true, 'SYSTEM'); + syslog_execute_alert_command($alert, $results, $hostname); } } } @@ -2421,4 +2430,3 @@ function alert_replace_variables($alert, $results, $hostname = '') { return $command; } - diff --git a/tests/regression/issue278_command_execution_refactor_test.php b/tests/regression/issue278_command_execution_refactor_test.php new file mode 100644 index 0000000..9ee8167 --- /dev/null +++ b/tests/regression/issue278_command_execution_refactor_test.php @@ -0,0 +1,118 @@ +