Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 +42 to +47
}
}
});
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 +45 to +48
});
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',
Comment on lines +16 to +17
'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