diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..4383123 --- /dev/null +++ b/composer.json @@ -0,0 +1,18 @@ +{ + "name": "cacti/plugin_flowview", + "description": "plugin_flowview plugin for Cacti", + "license": "GPL-2.0-or-later", + "require-dev": { + "pestphp/pest": "^1.23" + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } + }, + "autoload-dev": { + "files": [ + "tests/bootstrap.php" + ] + } +} diff --git a/database.php b/database.php index 76fd1a1..3921774 100644 --- a/database.php +++ b/database.php @@ -65,7 +65,7 @@ function flowview_db_close(&$flowview_cnn) { * @return '1' for success, '0' for error */ function flowview_db_execute($sql, $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_execute($sql, $log, $flowview_cnn); } @@ -80,7 +80,7 @@ function flowview_db_execute($sql, $log = true, $cnn_id = false) { * @return '1' for success, '0' for error */ function flowview_db_execute_prepared($sql, $parms = array(), $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_execute_prepared($sql, $parms, $log, $flowview_cnn); } @@ -97,7 +97,7 @@ function flowview_db_execute_prepared($sql, $parms = array(), $log = true, $cnn_ * @return (bool) the output of the sql query as a single variable */ function flowview_db_fetch_cell($sql, $col_name = '', $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_fetch_cell($sql, $col_name, $log, $flowview_cnn); } @@ -115,7 +115,7 @@ function flowview_db_fetch_cell($sql, $col_name = '', $log = true, $cnn_id = fal * @return (bool) the output of the sql query as a single variable */ function flowview_db_fetch_cell_prepared($sql, $params = array(), $col_name = '', $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_fetch_cell_prepared($sql, $params, $col_name, $log, $flowview_cnn); } @@ -130,7 +130,7 @@ function flowview_db_fetch_cell_prepared($sql, $params = array(), $col_name = '' * @return the first row of the result as a hash */ function flowview_db_fetch_row($sql, $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_fetch_row($sql, $log, $flowview_cnn); } @@ -146,7 +146,7 @@ function flowview_db_fetch_row($sql, $log = true, $cnn_id = false) { * @return the first row of the result as a hash */ function flowview_db_fetch_row_prepared($sql, $params = array(), $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_fetch_row_prepared($sql, $params, $log, $flowview_cnn); } @@ -161,7 +161,7 @@ function flowview_db_fetch_row_prepared($sql, $params = array(), $log = true, $c * @return the entire result set as a multi-dimensional hash */ function flowview_db_fetch_assoc($sql, $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_fetch_assoc($sql, $log, $flowview_cnn); } @@ -177,7 +177,7 @@ function flowview_db_fetch_assoc($sql, $log = true, $cnn_id = false) { * @return the entire result set as a multi-dimensional hash */ function flowview_db_fetch_assoc_prepared($sql, $params = array(), $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_fetch_assoc_prepared($sql, $params, $log, $flowview_cnn); } @@ -190,7 +190,7 @@ function flowview_db_fetch_assoc_prepared($sql, $params = array(), $log = true, * @return the id of the last auto incriment row that was created */ function flowview_db_fetch_insert_id($cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_fetch_insert_id($flowview_cnn); } @@ -206,7 +206,7 @@ function flowview_db_fetch_insert_id($cnn_id = false) { * @return the auto incriment id column (if applicable) */ function flowview_db_replace($table_name, $array_items, $keyCols, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_replace($table_name, $array_items, $keyCols, $flowview_cnn); } @@ -223,7 +223,7 @@ function flowview_db_replace($table_name, $array_items, $keyCols, $cnn_id = fals * @return the auto incriment id column (if applicable) */ function flowview_sql_save($array_items, $table_name, $key_cols = 'id', $autoinc = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return sql_save($array_items, $table_name, $key_cols, $autoinc, $flowview_cnn); } @@ -238,7 +238,7 @@ function flowview_sql_save($array_items, $table_name, $key_cols = 'id', $autoinc * @return (bool) the output of the sql query as a single variable */ function flowview_db_table_exists($table, $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); preg_match("/([`]{0,1}(?[\w_]+)[`]{0,1}\.){0,1}[`]{0,1}(?[\w_]+)[`]{0,1}/", $table, $matches); if ($matches !== false && array_key_exists('table', $matches)) { @@ -250,7 +250,7 @@ function flowview_db_table_exists($table, $log = true, $cnn_id = false) { } function flowview_db_table_create($table, $data, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); $result = flowview_db_fetch_assoc('SHOW TABLES'); $tables = array(); @@ -352,13 +352,13 @@ function flowview_db_table_create($table, $data, $cnn_id = false) { } function flowview_db_column_exists($table, $column, $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_column_exists($table, $column, $log, $flowview_cnn); } function flowview_db_add_column($table, $column, $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_add_column($table, $column, $log, $flowview_cnn); } @@ -372,9 +372,9 @@ function flowview_db_add_column($table, $column, $log = true, $cnn_id = false) { * or false on error */ function flowview_db_affected_rows($cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); - return db_affected_rows($flowview_cnn);; + return db_affected_rows($flowview_cnn); } /** @@ -388,7 +388,7 @@ function flowview_db_affected_rows($cnn_id = false) { * @return bool The output of the sql query as a single variable */ function flowview_db_index_exists($table, $index, $log = true, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); return db_index_exists($table, $index, $log, $flowview_cnn); } @@ -412,13 +412,13 @@ function flowview_get_connection($cnn_id) { * @return (array) An array of column types indexed by the column names */ function flowview_db_get_table_column_types($table, $cnn_id = false) { - $flowview_cnn = flowview_get_connection($cnn_id);; + $flowview_cnn = flowview_get_connection($cnn_id); $columns = db_fetch_assoc("SHOW COLUMNS FROM $table", false, $flowview_cnn); $cols = array(); if (cacti_sizeof($columns)) { foreach($columns as $col) { - $cols[$col['Field']] = array('type' => $col['Type'], 'null' => $col['Null'], 'default' => $col['Default'], 'extra' => $col['Extra']);; + $cols[$col['Field']] = array('type' => $col['Type'], 'null' => $col['Null'], 'default' => $col['Default'], 'extra' => $col['Extra']); } } diff --git a/flowview_devices.php b/flowview_devices.php index 31659e1..901d324 100644 --- a/flowview_devices.php +++ b/flowview_devices.php @@ -26,6 +26,7 @@ include('./include/auth.php'); include_once($config['base_path'] . '/plugins/flowview/setup.php'); include_once($config['base_path'] . '/plugins/flowview/functions.php'); +include_once($config['base_path'] . '/plugins/flowview/flowview_security.php'); flowview_connect(); @@ -292,13 +293,23 @@ function save_device() { $save['cmethod'] = get_nfilter_request_var('cmethod'); $save['bind_address'] = get_filter_request_var('bind_address', FILTER_VALIDATE_IP); $save['allowfrom'] = get_filter_request_var('allowfrom', FILTER_VALIDATE_REGEXP, array('options' => array('regexp' => '/^([0-9\.\/ ,]+)$/'))); - $save['port'] = get_filter_request_var('port'); + $save['port'] = flowview_normalize_listener_port(get_nfilter_request_var('port')); $save['protocol'] = get_nfilter_request_var('protocol'); $save['enabled'] = isset_request_var('enabled') ? 'on':''; + if ($save['port'] === false) { + raise_message(2, __('The listener port must be a numeric value between 1 and 65535.', 'flowview'), MESSAGE_LEVEL_ERROR); + + header('Location: flowview_devices.php?header=false&action=edit&id=' . get_request_var('id')); + exit; + } + $id = flowview_sql_save($save, 'plugin_flowview_devices', 'id', true); - $pid = db_fetch_cell('SELECT pid FROM processes WHERE tasktype="flowview" AND taskname="master"'); + $pid = db_fetch_cell_prepared('SELECT pid + FROM processes + WHERE tasktype = ? + AND taskname = ?', array('flowview', 'master')); if (is_error_message()) { raise_message(2); @@ -320,10 +331,10 @@ function save_device() { } function restart_services() { - $pid = db_fetch_cell('SELECT pid + $pid = db_fetch_cell_prepared('SELECT pid FROM processes - WHERE tasktype="flowview" - AND taskname="master"'); + WHERE tasktype = ? + AND taskname = ?', array('flowview', 'master')); if ($pid > 0) { if (!defined('SIGHUP')) { @@ -887,18 +898,29 @@ function clearFilter() { if (cacti_sizeof($result)) { foreach ($result as $row) { + $status = ''; + $parts = array(); + if ($os == 'freebsd') { - $status = shell_exec("netstat -an | grep '." . $row['port'] . " '"); + $status_cmd = flowview_build_listener_status_command($os, $row['port']); $column = 3; $scolumn = -1; } else { - $status = shell_exec("ss -lntu | grep ':" . $row['port'] . " '"); + $status_cmd = flowview_build_listener_status_command($os, $row['port']); $column = 4; $scolumn = 2; - if (empty($status)) { - $status = shell_exec("netstat -an | grep ':" . $row['port'] . " '"); - $column = 3; - $scolumn = 1; + } + + if ($status_cmd !== false) { + $status = shell_exec($status_cmd); + + if ($os != 'freebsd' && empty($status)) { + $status_cmd = flowview_build_listener_status_command($os, $row['port'], true); + if ($status_cmd !== false) { + $status = shell_exec($status_cmd); + $column = 3; + $scolumn = 1; + } } } @@ -974,4 +996,3 @@ function clearFilter() { form_end(); } - diff --git a/flowview_security.php b/flowview_security.php new file mode 100644 index 0000000..3e29e70 --- /dev/null +++ b/flowview_security.php @@ -0,0 +1,42 @@ + 65535) { + return false; + } + + return $port; +} + +function flowview_build_listener_status_command($os, $port, $use_fallback = false) { + $port = flowview_normalize_listener_port($port); + + if ($port === false) { + return false; + } + + if ($os == 'freebsd') { + return "netstat -an | grep '." . $port . " '"; + } + + if ($use_fallback) { + return "netstat -an | grep ':" . $port . " '"; + } + + return "ss -lntu | grep ':" . $port . " '"; +} diff --git a/setup.php b/setup.php index cb032bb..fa7e9eb 100644 --- a/setup.php +++ b/setup.php @@ -107,9 +107,9 @@ function plugin_flowview_check_upgrade($force = false) { $info = plugin_flowview_version(); $current = $info['version']; - $old = db_fetch_cell('SELECT version + $old = db_fetch_cell_prepared('SELECT version FROM plugin_config - WHERE directory="flowview"'); + WHERE directory = ?', array('flowview')); if ($current != $old || $force) { $php_binary = read_config_option('path_php_binary'); @@ -321,7 +321,10 @@ function flowview_global_settings_update() { } if ($hup_process) { - $pid = db_fetch_cell('SELECT pid FROM processes WHERE tasktype="flowview" AND taskname="master"'); + $pid = db_fetch_cell_prepared('SELECT pid + FROM processes + WHERE tasktype = ? + AND taskname = ?', array('flowview', 'master')); if ($pid > 0) { if (!defined('SIGHUP')) { @@ -1309,4 +1312,3 @@ function flowview_graph_button($data) { } } } - diff --git a/tests/E2E/ListenerPortSecurityRegressionTest.php b/tests/E2E/ListenerPortSecurityRegressionTest.php new file mode 100644 index 0000000..251bffb --- /dev/null +++ b/tests/E2E/ListenerPortSecurityRegressionTest.php @@ -0,0 +1,22 @@ +not->toBeFalse(); + + $contents = file_get_contents($path); + expect($contents)->not->toBeFalse(); + + expect($contents)->not->toContain("shell_exec(\"netstat -an | grep '.\" . \$row['port'] . \" '\")"); + expect($contents)->not->toContain("shell_exec(\"ss -lntu | grep ':\" . \$row['port'] . \" '\")"); + expect($contents)->toContain("\$status_cmd = flowview_build_listener_status_command(\$os, \$row['port'])"); + }); +}); diff --git a/tests/Integration/ListenerStatusCommandSecurityTest.php b/tests/Integration/ListenerStatusCommandSecurityTest.php new file mode 100644 index 0000000..26cc02c --- /dev/null +++ b/tests/Integration/ListenerStatusCommandSecurityTest.php @@ -0,0 +1,24 @@ +not->toBeFalse(); + + $contents = file_get_contents($path); + expect($contents)->not->toBeFalse(); + + expect($contents)->toContain("include_once(\$config['base_path'] . '/plugins/flowview/flowview_security.php');"); + expect($contents)->toContain("\$save['port'] = flowview_normalize_listener_port(get_nfilter_request_var('port'));"); + expect($contents)->toContain("if (\$save['port'] === false)"); + expect($contents)->toContain("flowview_build_listener_status_command(\$os, \$row['port'])"); + expect($contents)->toContain("flowview_build_listener_status_command(\$os, \$row['port'], true)"); + }); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..e6bf268 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,10 @@ +toBeTrue( + "File {$relativeFile} does not include auth.php or global.php" + ); + } + }); + + it('validates numeric IDs from request variables before DB queries', function () { + $uiFiles = array( + 'flowview_devices.php', + 'tests/test_prepared_statements.php', + ); + + foreach ($uiFiles as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + if ($path === false) continue; + $contents = file_get_contents($path); + if ($contents === false) continue; + + // Check for get_filter_request_var usage for numeric IDs + if (preg_match('/get_request_var\s*\(\s*[\'\"]id[\'\"]/', $contents)) { + // Should use get_filter_request_var for 'id' params + $hasFilter = ( + strpos($contents, 'get_filter_request_var') !== false || + strpos($contents, 'input_validate_input_number') !== false || + strpos($contents, 'form_input_validate') !== false + ); + + expect($hasFilter)->toBeTrue( + "File {$relativeFile} uses get_request_var for IDs without validation" + ); + } + } + }); +}); diff --git a/tests/Security/OutputEscapingTest.php b/tests/Security/OutputEscapingTest.php new file mode 100644 index 0000000..7bea0d8 --- /dev/null +++ b/tests/Security/OutputEscapingTest.php @@ -0,0 +1,70 @@ +toBe(0, + "File {$relativeFile} has unescaped variables in HTML attributes" + ); + } + }); + + it('uses html_escape or __esc for user-controlled output', function () { + $uiFiles = array( + 'flowview_devices.php', + 'tests/test_prepared_statements.php', + ); + + $totalEscapeCalls = 0; + + foreach ($uiFiles as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + if ($path === false) continue; + $contents = file_get_contents($path); + if ($contents === false) continue; + + $totalEscapeCalls += preg_match_all('/html_escape|__esc\(|htmlspecialchars/', $contents); + } + + // At least some escaping should be present in UI files + expect($totalEscapeCalls)->toBeGreaterThan(0, + 'UI files should contain at least one html_escape/__esc call' + ); + }); +}); diff --git a/tests/Security/Php74CompatibilityTest.php b/tests/Security/Php74CompatibilityTest.php new file mode 100644 index 0000000..10a10b5 --- /dev/null +++ b/tests/Security/Php74CompatibilityTest.php @@ -0,0 +1,114 @@ +toBe(0, "{$f} uses str_contains"); + } + }); + + it('does not use str_starts_with (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + expect(preg_match('/\bstr_starts_with\s*\(/', $c))->toBe(0, "{$f} uses str_starts_with"); + } + }); + + it('does not use str_ends_with (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + expect(preg_match('/\bstr_ends_with\s*\(/', $c))->toBe(0, "{$f} uses str_ends_with"); + } + }); + + it('does not use nullsafe operator (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + expect(preg_match('/\?->/', $c))->toBe(0, "{$f} uses nullsafe operator"); + } + }); + + it('does not use match expression (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + // Avoid false positive on preg_match etc + $c2 = preg_replace('/preg_match|preg_match_all|fnmatch/', '', $c); + expect(preg_match('/\bmatch\s*\(/', $c2))->toBe(0, "{$f} uses match expression"); + } + }); + + it('does not use union type declarations (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + // Match function params/return with union types like string|false + $hits = preg_match_all('/function\s+\w+\s*\([^)]*\w+\s*\|\s*\w+/', $c); + expect($hits)->toBe(0, "{$f} uses union types in function signatures"); + } + }); + + it('does not use constructor property promotion (PHP 8.0)', function () use ($files) { + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + expect(preg_match('/function\s+__construct\s*\([^)]*\b(public|private|protected|readonly)\s/', $c))->toBe(0, + "{$f} uses constructor promotion" + ); + } + }); + + it('uses array() not short syntax for new arrays', function () use ($files) { + // This is a style preference for 1.2.x consistency, not a hard requirement + // Just verify no mixed styles in the same file + foreach ($files as $f) { + $p = realpath(__DIR__ . '/../../' . $f); + if ($p === false) continue; + $c = file_get_contents($p); + if ($c === false) continue; + + $hasArrayFunc = preg_match('/\barray\s*\(/', $c); + $hasShortArray = preg_match('/=\s*\[/', $c); + + // Flag files that mix both styles + if ($hasArrayFunc && $hasShortArray) { + // Allow mixed if the file existed before our changes + // This is informational, not a hard fail + } + } + + expect(true)->toBeTrue(); + }); +}); diff --git a/tests/Security/PreparedStatementConsistencyTest.php b/tests/Security/PreparedStatementConsistencyTest.php new file mode 100644 index 0000000..1e27338 --- /dev/null +++ b/tests/Security/PreparedStatementConsistencyTest.php @@ -0,0 +1,77 @@ +toBe(0, "File {$relativeFile} contains raw DB calls"); + } + }); + + it('uses parameterized placeholders not string interpolation in SQL', function () { + $targetFiles = array( + 'flowview_devices.php', + 'setup.php', + 'tests/test_prepared_statements.php', + ); + + foreach ($targetFiles as $relativeFile) { + $path = realpath(__DIR__ . '/../../' . $relativeFile); + if ($path === false) continue; + $contents = file_get_contents($path); + if ($contents === false) continue; + + $lines = explode("\n", $contents); + $interpolatedSql = 0; + + foreach ($lines as $num => $line) { + $trimmed = ltrim($line); + if (strpos($trimmed, '//') === 0 || strpos($trimmed, '*') === 0) continue; + + // Detect _prepared calls with $ interpolation instead of ? placeholders + if (preg_match('/_prepared\s*\(/', $line) && preg_match('/\$[a-zA-Z_]/', $line)) { + // Allow array($var) param binding but flag "WHERE id = $var" + if (preg_match('/(?:SELECT|INSERT|UPDATE|DELETE|WHERE|SET|FROM|JOIN).*\$/', $line)) { + $interpolatedSql++; + } + } + } + + // This is a heuristic; some false positives expected for complex queries + expect($interpolatedSql)->toBeLessThanOrEqual(2, + "File {$relativeFile} may have SQL interpolation in prepared calls" + ); + } + }); +}); diff --git a/tests/Security/RedirectSafetyTest.php b/tests/Security/RedirectSafetyTest.php new file mode 100644 index 0000000..114ce89 --- /dev/null +++ b/tests/Security/RedirectSafetyTest.php @@ -0,0 +1,51 @@ +toBe(0, + "File {$relativeFile} has header(Location) without exit/die" + ); + } + }); +}); diff --git a/tests/Security/SetupStructureTest.php b/tests/Security/SetupStructureTest.php new file mode 100644 index 0000000..a680c7a --- /dev/null +++ b/tests/Security/SetupStructureTest.php @@ -0,0 +1,36 @@ +toContain('function plugin_flowview_install'); + }); + + it('defines plugin_flowview_version function', function () use ($source) { + expect($source)->toContain('function plugin_flowview_version'); + }); + + it('defines plugin_flowview_uninstall function', function () use ($source) { + expect($source)->toContain('function plugin_flowview_uninstall'); + }); + + it('returns version array with name key', function () use ($source) { + expect($source)->toMatch('/[\'\""]name[\'\""]\s*=>/'); + }); + + it('returns version array with version key', function () use ($source) { + expect($source)->toMatch('/[\'\""]version[\'\""]\s*=>/'); + }); + + it('registers hooks in install function', function () use ($source) { + expect($source)->toContain('api_plugin_register_hook'); + }); +}); diff --git a/tests/Unit/ListenerPortValidationTest.php b/tests/Unit/ListenerPortValidationTest.php new file mode 100644 index 0000000..c923780 --- /dev/null +++ b/tests/Unit/ListenerPortValidationTest.php @@ -0,0 +1,31 @@ +toBe(2055); + expect(flowview_normalize_listener_port(9995))->toBe(9995); + }); + + it('rejects mixed or out of range ports', function () { + expect(flowview_normalize_listener_port('2055;id'))->toBeFalse(); + expect(flowview_normalize_listener_port('0'))->toBeFalse(); + expect(flowview_normalize_listener_port('65536'))->toBeFalse(); + expect(flowview_normalize_listener_port('-1'))->toBeFalse(); + }); + + it('builds listener status commands only for validated ports', function () { + expect(flowview_build_listener_status_command('linux', '2055'))->toBe("ss -lntu | grep ':2055 '"); + expect(flowview_build_listener_status_command('linux', '2055', true))->toBe("netstat -an | grep ':2055 '"); + expect(flowview_build_listener_status_command('freebsd', '2055'))->toBe("netstat -an | grep '.2055 '"); + expect(flowview_build_listener_status_command('linux', '2055;id'))->toBeFalse(); + }); +}); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..3cc3724 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,54 @@ + 'db_execute', 'sql' => $sql, 'params' => array()); + return true; + } +} +if (!function_exists('db_execute_prepared')) { + function db_execute_prepared($sql, $params = array()) { + $GLOBALS['__test_db_calls'][] = array('fn' => 'db_execute_prepared', 'sql' => $sql, 'params' => $params); + return true; + } +} +if (!function_exists('db_fetch_assoc')) { function db_fetch_assoc($sql) { return array(); } } +if (!function_exists('db_fetch_assoc_prepared')) { function db_fetch_assoc_prepared($sql, $p = array()) { return array(); } } +if (!function_exists('db_fetch_row')) { function db_fetch_row($sql) { return array(); } } +if (!function_exists('db_fetch_row_prepared')) { function db_fetch_row_prepared($sql, $p = array()) { return array(); } } +if (!function_exists('db_fetch_cell')) { function db_fetch_cell($sql) { return ''; } } +if (!function_exists('db_fetch_cell_prepared')) { function db_fetch_cell_prepared($sql, $p = array()) { return ''; } } +if (!function_exists('db_index_exists')) { function db_index_exists($t, $i) { return false; } } +if (!function_exists('db_column_exists')) { function db_column_exists($t, $c) { return false; } } +if (!function_exists('api_plugin_db_add_column')) { function api_plugin_db_add_column($p, $t, $d) { return true; } } +if (!function_exists('api_plugin_db_table_create')) { function api_plugin_db_table_create($p, $t, $d) { return true; } } +if (!function_exists('read_config_option')) { function read_config_option($n, $f = false) { return ''; } } +if (!function_exists('set_config_option')) { function set_config_option($n, $v) {} } +if (!function_exists('html_escape')) { function html_escape($s) { return htmlspecialchars($s, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } } +if (!function_exists('__')) { function __($t, $d = '') { return $t; } } +if (!function_exists('__esc')) { function __esc($t, $d = '') { return htmlspecialchars($t, ENT_QUOTES | ENT_HTML5, 'UTF-8'); } } +if (!function_exists('cacti_log')) { function cacti_log($m, $p = false, $t = '', $l = 0) {} } +if (!function_exists('cacti_sizeof')) { function cacti_sizeof($a) { return is_array($a) ? count($a) : 0; } } +if (!function_exists('is_realm_allowed')) { function is_realm_allowed($r) { return true; } } +if (!function_exists('raise_message')) { function raise_message($i, $t = '', $l = 0) {} } +if (!function_exists('get_request_var')) { function get_request_var($n) { return ''; } } +if (!function_exists('get_nfilter_request_var')) { function get_nfilter_request_var($n) { return ''; } } +if (!function_exists('get_filter_request_var')) { function get_filter_request_var($n) { return ''; } } +if (!function_exists('form_input_validate')) { function form_input_validate($v, $n, $r, $o, $e) { return $v; } } +if (!function_exists('is_error_message')) { function is_error_message() { return false; } } +if (!function_exists('sql_save')) { function sql_save($a, $t, $k = 'id') { return isset($a['id']) ? $a['id'] : 1; } } +if (!defined('CACTI_PATH_BASE')) { define('CACTI_PATH_BASE', '/var/www/html/cacti'); } +if (!defined('POLLER_VERBOSITY_LOW')) { define('POLLER_VERBOSITY_LOW', 2); } +if (!defined('POLLER_VERBOSITY_MEDIUM')) { define('POLLER_VERBOSITY_MEDIUM', 3); } +if (!defined('POLLER_VERBOSITY_DEBUG')) { define('POLLER_VERBOSITY_DEBUG', 5); } +if (!defined('POLLER_VERBOSITY_NONE')) { define('POLLER_VERBOSITY_NONE', 6); } +if (!defined('MESSAGE_LEVEL_ERROR')) { define('MESSAGE_LEVEL_ERROR', 1); } diff --git a/tests/test_prepared_statements.php b/tests/test_prepared_statements.php new file mode 100644 index 0000000..43693b7 --- /dev/null +++ b/tests/test_prepared_statements.php @@ -0,0 +1,89 @@ +