Skip to content
14 changes: 14 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Pest configuration file.
*/

require_once __DIR__ . '/bootstrap.php';
77 changes: 77 additions & 0 deletions tests/Security/Php74CompatibilityTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify plugin source files do not use PHP 8.0+ syntax.
* Cacti 1.2.x plugins must remain compatible with PHP 7.4.
*/
Comment on lines +10 to +13

function thold_security_compatibility_files() {
return array(
'includes/database.php',
'includes/polling.php',
'includes/settings.php',
'notify_lists.php',
'notify_queue.php',
'poller_thold.php',
'setup.php',
'thold.php',
'thold_graph.php',
);
}

function thold_security_read_file($relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);
expect($path)->not->toBeFalse("Failed to resolve target file path: {$relativeFile}");

$contents = file_get_contents($path);
expect($contents)->not->toBeFalse("Failed to read target file: {$relativeFile}");

return $contents;
}

it('does not use str_contains (PHP 8.0)', function () {
foreach (thold_security_compatibility_files() as $relativeFile) {
$contents = thold_security_read_file($relativeFile);

expect(preg_match('/\bstr_contains\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_contains() which requires PHP 8.0"
);
}
});

it('does not use str_starts_with (PHP 8.0)', function () {
foreach (thold_security_compatibility_files() as $relativeFile) {
$contents = thold_security_read_file($relativeFile);

expect(preg_match('/\bstr_starts_with\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_starts_with() which requires PHP 8.0"
);
}
});

it('does not use str_ends_with (PHP 8.0)', function () {
foreach (thold_security_compatibility_files() as $relativeFile) {
$contents = thold_security_read_file($relativeFile);

expect(preg_match('/\bstr_ends_with\s*\(/', $contents))->toBe(0,
"{$relativeFile} uses str_ends_with() which requires PHP 8.0"
);
}
});

it('does not use nullsafe operator (PHP 8.0)', function () {
foreach (thold_security_compatibility_files() as $relativeFile) {
$contents = thold_security_read_file($relativeFile);

expect(preg_match('/\?->/', $contents))->toBe(0,
"{$relativeFile} uses nullsafe operator which requires PHP 8.0"
);
}
});
50 changes: 50 additions & 0 deletions tests/Security/PreparedStatementConsistencyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify files in the hardening stack use prepared helpers when they execute
* obviously variable-interpolated SQL on a single line.
*/

it('does not introduce single-line interpolated db_* calls in hardened files', function () {
$targetFiles = array(
'poller_thold.php',
'setup.php',
'thold.php',
'thold_graph.php',
);

$rawInterpolatedPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)\s*\(\s*(["\']).*\$[A-Za-z_{]/';
$preparedPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)_prepared\s*\(/';

foreach ($targetFiles as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);
expect($path)->not->toBeFalse("Failed to resolve target file path: {$relativeFile}");

$contents = file_get_contents($path);
expect($contents)->not->toBeFalse("Failed to read target file: {$relativeFile}");

$lines = explode("\n", $contents);

foreach ($lines as $lineNumber => $line) {
$trimmed = ltrim($line);

if (strpos($trimmed, '//') === 0 || strpos($trimmed, '*') === 0 || strpos($trimmed, '#') === 0) {
continue;
}

$hasInterpolatedRawCall = preg_match($rawInterpolatedPattern, $line) === 1;
$hasPreparedCall = preg_match($preparedPattern, $line) === 1;

expect($hasInterpolatedRawCall && !$hasPreparedCall)->toBeFalse(
sprintf('File %s contains an interpolated raw db_* call at line %d', $relativeFile, $lineNumber + 1)
);
Comment on lines +91 to +96
}
}
});
49 changes: 49 additions & 0 deletions tests/Security/SetupStructureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Verify setup.php defines required plugin hooks and info function.
*/

function thold_read_setup_source() {
$setupPath = realpath(__DIR__ . '/../../setup.php');
expect($setupPath)->not->toBeFalse('Failed to resolve setup.php');
expect(is_readable($setupPath))->toBeTrue('setup.php is not readable');

$source = file_get_contents($setupPath);
expect($source)->not->toBeFalse('Failed to read setup.php');

return $source;
}

it('defines plugin_thold_install function', function () {
$source = thold_read_setup_source();
expect($source)->toContain('function plugin_thold_install');
});

it('defines plugin_thold_version function', function () {
$source = thold_read_setup_source();
expect($source)->toContain('function plugin_thold_version');
});

it('defines plugin_thold_uninstall function', function () {
$source = thold_read_setup_source();
expect($source)->toContain('function plugin_thold_uninstall');
});

it('returns version array with name key', function () {
$source = thold_read_setup_source();
expect($source)->toMatch('/[\'\""]name[\'\""]\s*=>/');
});

it('reads plugin metadata from INFO and returns the info section', function () {
$source = thold_read_setup_source();
expect($source)->toContain('parse_ini_file');
expect($source)->toContain("return \$info['info'];");
Comment on lines +43 to +46
});
205 changes: 205 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

/*
* Test bootstrap: stub Cacti framework functions so plugin code
* can be loaded in isolation without the full Cacti application.
*/

$GLOBALS['__test_db_calls'] = array();
$GLOBALS['config'] = array(
'base_path' => '/var/www/html/cacti',
'url_path' => '/cacti/',
'cacti_version' => '1.2.999',
);

if (!function_exists('db_execute')) {
function db_execute($sql) {
$GLOBALS['__test_db_calls'][] = array('fn' => '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, $params = 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, $params = 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, $params = array()) {
return '';
}
}

if (!function_exists('db_index_exists')) {
function db_index_exists($table, $index) {
return false;
}
}

if (!function_exists('db_column_exists')) {
function db_column_exists($table, $column) {
return false;
}
}

if (!function_exists('api_plugin_db_add_column')) {
function api_plugin_db_add_column($plugin, $table, $data) {
return true;
}
}

if (!function_exists('api_plugin_db_table_create')) {
function api_plugin_db_table_create($plugin, $table, $data) {
return true;
}
}

if (!function_exists('read_config_option')) {
function read_config_option($name, $force = false) {
return '';
}
}

if (!function_exists('set_config_option')) {
function set_config_option($name, $value) {
}
}

if (!function_exists('html_escape')) {
function html_escape($string) {
return htmlspecialchars($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
}

if (!function_exists('__')) {
function __($text, $domain = '') {
return $text;
}
}

if (!function_exists('__esc')) {
function __esc($text, $domain = '') {
return htmlspecialchars($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
}
}

if (!function_exists('cacti_log')) {
function cacti_log($message, $also_print = false, $log_type = '', $level = 0) {
}
}

if (!function_exists('cacti_sizeof')) {
function cacti_sizeof($array) {
return is_array($array) ? count($array) : 0;
}
}

if (!function_exists('is_realm_allowed')) {
function is_realm_allowed($realm) {
return true;
}
}

if (!function_exists('raise_message')) {
function raise_message($id, $text = '', $level = 0) {
}
}

if (!function_exists('get_request_var')) {
function get_request_var($name) {
return '';
}
}

if (!function_exists('get_nfilter_request_var')) {
function get_nfilter_request_var($name) {
return '';
}
}

if (!function_exists('get_filter_request_var')) {
function get_filter_request_var($name) {
return '';
}
}

if (!function_exists('form_input_validate')) {
function form_input_validate($value, $name, $regex, $optional, $error) {
return $value;
}
}

if (!function_exists('is_error_message')) {
function is_error_message() {
return false;
}
}

if (!function_exists('sql_save')) {
function sql_save($array, $table, $key = 'id') {
return isset($array['id']) ? $array['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);
}
Loading