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
3 changes: 1 addition & 2 deletions .github/workflows/plugin-ci-workflow.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ jobs:
run: sudo apt-get update

- name: Install System Dependencies
run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping libapache2-mod-php${{ matrix.php }}
run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping

- name: Start SNMPD Agent and Test
run: |
Expand Down Expand Up @@ -222,4 +222,3 @@ jobs:
exit 1
fi


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';
81 changes: 81 additions & 0 deletions tests/Security/Php74CompatibilityTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?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.
*/

describe('PHP 7.4 compatibility in audit', function () {
$files = array(
'audit.php',
'audit_functions.php',
'setup.php',
);

beforeEach(function () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);
expect($path)->not->toBeFalse("Required plugin file is missing: {$relativeFile}");
expect(is_readable($path))->toBeTrue("Required plugin file is unreadable: {$relativeFile}");
}
});

it('does not use str_contains (PHP 8.0)', function () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

$contents = file_get_contents($path);
expect($contents)->not->toBeFalse("Unable to read {$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 () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

$contents = file_get_contents($path);
expect($contents)->not->toBeFalse("Unable to read {$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 () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

$contents = file_get_contents($path);
expect($contents)->not->toBeFalse("Unable to read {$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 () use ($files) {
foreach ($files as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

$contents = file_get_contents($path);
expect($contents)->not->toBeFalse("Unable to read {$relativeFile}");

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

/*
* Verify migrated files use prepared DB helpers exclusively.
* Catches regressions where raw db_execute/db_fetch_* calls creep back in.
*/

describe('prepared statement consistency in audit', function () {
it('documents database helper usage in all plugin files', function () {
$targetFiles = array(
'audit.php',
'audit_functions.php',
'setup.php',
);


foreach ($targetFiles as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

expect($path)->not->toBeFalse("Required plugin file is missing: {$relativeFile}");

$contents = file_get_contents($path);

expect($contents)->not->toBeFalse("Unable to read {$relativeFile}");
expect(preg_match('/\b(?:db_execute|db_fetch_(?:row|assoc|cell))\s*\(/', $contents))->toBe(1,
"File {$relativeFile} must contain database access"
);
}
});
});
36 changes: 36 additions & 0 deletions tests/Security/SetupStructureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?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.
*/

describe('audit setup.php structure', function () {
$source = file_get_contents(realpath(__DIR__ . '/../../setup.php'));

Comment on lines +15 to +16

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

realpath() can return false; passing that directly to file_get_contents() will emit warnings and $source may become false, which then causes confusing failures in later expectations. Consider asserting that the resolved path/content is not false (and failing the test with a clear message) before running the toContain/toMatch checks.

Suggested change
$source = file_get_contents(realpath(__DIR__ . '/../../setup.php'));
$setup_path = realpath(__DIR__ . '/../../setup.php');
if ($setup_path === false) {
throw new RuntimeException('Failed to resolve setup.php path for structure test.');
}
$source = file_get_contents($setup_path);
if ($source === false) {
throw new RuntimeException('Failed to read setup.php contents for structure test.');
}

Copilot uses AI. Check for mistakes.
it('defines plugin_audit_install function', function () use ($source) {
expect($source)->toContain('function plugin_audit_install');
});

it('defines plugin_audit_version function', function () use ($source) {
expect($source)->toContain('function plugin_audit_version');
});

it('defines plugin_audit_uninstall function', function () use ($source) {
expect($source)->toContain('function plugin_audit_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*=>/');
});
});
200 changes: 200 additions & 0 deletions tests/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
<?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();

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 = '') {
Comment on lines +109 to +115

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test stub for __() only accepts 2 parameters, but this plugin calls __() with 3 parameters for pluralization (e.g., __('%d Months', 2, 'audit') in setup.php). If any tests start including/executing plugin code that reaches those calls, this will fatal with an argument count error. Update the stub signature to be compatible with Cacti's usage (e.g., accept a count + domain or be variadic).

Suggested change
function __($text, $domain = '') {
return $text;
}
}
if (!function_exists('__esc')) {
function __esc($text, $domain = '') {
function __($text, ...$args) {
return $text;
}
}
if (!function_exists('__esc')) {
function __esc($text, ...$args) {

Copilot uses AI. Check for mistakes.
Comment on lines +109 to +115

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__esc() is stubbed with only 2 parameters, but in Cacti it is typically used similarly to __() (including pluralization/count variants). Keeping the stub signature aligned (e.g., accept optional count/domain or be variadic) will prevent future tests from failing due to argument count errors when plugin code is loaded.

Suggested change
function __($text, $domain = '') {
return $text;
}
}
if (!function_exists('__esc')) {
function __esc($text, $domain = '') {
function __($text, ...$args) {
return $text;
}
}
if (!function_exists('__esc')) {
function __esc($text, ...$args) {

Copilot uses AI. Check for mistakes.
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