Skip to content
Draft
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
12 changes: 12 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10
6 changes: 4 additions & 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 @@ -195,7 +195,9 @@ jobs:
working-directory: ${{ github.workspace }}/cacti

- name: Run PHPStan at Level 6 on base code outside of Composer due to technical issues
run: ./include/vendor/bin/phpstan analyze --level 6 ${{ github.workspace }}/cacti/plugins/maint
run: |
find ${{ github.workspace }}/cacti/plugins/maint -type f -name '*.php' ! -path '*/tests/*' -print0 |
xargs -0 ./include/vendor/bin/phpstan analyze --level 6
working-directory: ${{ github.workspace }}/cacti

- name: Run Cacti Poller
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@
# +-------------------------------------------------------------------------+

locales/po/*.mo
.omc/
21 changes: 21 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "cacti/plugin_maint",
"description": "plugin_maint 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"
]

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

This PR adds a Pest test suite, but the repo’s CI workflow doesn’t run vendor/bin/pest today (it only runs the Cacti integration workflow). Consider adding a scripts entry (e.g., a test script) and/or updating CI to execute the Pest suite so these security/compatibility checks are actually enforced.

Suggested change
]
]
},
"scripts": {
"test": "vendor/bin/pest"

Copilot uses AI. Check for mistakes.
},
"scripts": {
"test": "vendor/bin/pest"
}
}
12 changes: 6 additions & 6 deletions maint.php
Original file line number Diff line number Diff line change
Expand Up @@ -924,9 +924,9 @@ function changemaintType () {
function schedules(): void {
global $actions, $maint_types, $maint_intervals, $yesno;

$schedules = db_fetch_assoc('SELECT *
$schedules = db_fetch_assoc_prepared('SELECT *
FROM plugin_maint_schedules
ORDER BY name');
ORDER BY name', []);

form_start('maint.php', 'chk');

Expand Down Expand Up @@ -1134,10 +1134,10 @@ function clearFilter() {
<option value='<?php print MAINT_HOST_FILTER_ANY ?>' <?php if (get_request_var('site_id') == MAINT_HOST_FILTER_ANY) {?> selected<?php }?>><?php print __('Any', 'maint'); ?></option>
<option value='<?php print MAINT_HOST_FILTER_NONE ?>' <?php if (get_request_var('site_id') == MAINT_HOST_FILTER_NONE) {?> selected<?php }?>><?php print __('None', 'maint'); ?></option>
<?php
$sites = db_fetch_assoc('SELECT id, name
$sites = db_fetch_assoc_prepared('SELECT id, name
FROM sites
WHERE id IN (SELECT site_id FROM host)
ORDER BY name');
ORDER BY name', []);

if (cacti_sizeof($sites)) {
foreach ($sites as $site) {
Expand All @@ -1158,9 +1158,9 @@ function clearFilter() {
<select id='poller_id'>
<option value='<?php print MAINT_HOST_FILTER_ANY ?>' <?php if (get_request_var('poller_id') == MAINT_HOST_FILTER_ANY) {?> selected<?php }?>><?php print __('Any', 'maint'); ?></option>
<?php
$pollers = db_fetch_assoc('SELECT id, name
$pollers = db_fetch_assoc_prepared('SELECT id, name
FROM poller
ORDER BY name');
ORDER BY name', []);

if (cacti_sizeof($pollers)) {
foreach ($pollers as $poller) {
Expand Down
4 changes: 2 additions & 2 deletions setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -298,9 +298,9 @@ function maint_device_action_prepare(array $save): array {
}

// Load schedules to choose from
$schedules = db_fetch_assoc('SELECT id, name, enabled, mtype, stime, etime, minterval
$schedules = db_fetch_assoc_prepared('SELECT id, name, enabled, mtype, stime, etime, minterval
FROM plugin_maint_schedules
ORDER BY name');
ORDER BY name', []);

$select = "<select name='maint_schedule_id' style='min-width:360px'>";

Expand Down
12 changes: 12 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?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.
*/

describe('PHP 7.4 compatibility in maint', function () {
$files = [
'functions.php',
'maint.php',
'setup.php',
];

$readRequiredFile = function (string $relativeFile): string {
$path = realpath(__DIR__ . '/../../' . $relativeFile);

if ($path === false) {
throw new RuntimeException("Unable to resolve required file: {$relativeFile}");
}

$contents = file_get_contents($path);

if ($contents === false) {
throw new RuntimeException("Unable to read required file: {$relativeFile}");
}

return $contents;
};

it('does not use str_contains (PHP 8.0)', function () use ($files, $readRequiredFile) {
foreach ($files as $relativeFile) {
$contents = $readRequiredFile($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, $readRequiredFile) {
foreach ($files as $relativeFile) {
$contents = $readRequiredFile($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, $readRequiredFile) {
foreach ($files as $relativeFile) {
$contents = $readRequiredFile($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, $readRequiredFile) {
foreach ($files as $relativeFile) {
$contents = $readRequiredFile($relativeFile);

expect(preg_match('/\?->/', $contents))->toBe(0,
"{$relativeFile} uses nullsafe operator which requires PHP 8.0"
);
}
});
});
59 changes: 59 additions & 0 deletions tests/Security/PreparedStatementConsistencyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?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 maint', function () {
it('uses prepared DB helpers in all plugin files', function () {
$targetFiles = [
'functions.php',
'maint.php',
'setup.php',
Comment on lines +16 to +20

Copilot AI Apr 9, 2026

Copy link

Choose a reason for hiding this comment

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

This test currently fails against the repository’s current code: maint.php and setup.php contain raw db_fetch_assoc( calls (e.g. maint.php:927, 1137, 1161 and setup.php:301). Either migrate those calls to db_fetch_assoc_prepared() (preferred) or adjust targetFiles/add an allowlist for known-safe cases so the test reflects the intended enforcement scope.

Suggested change
it('uses prepared DB helpers in all plugin files', function () {
$targetFiles = array(
'functions.php',
'maint.php',
'setup.php',
it('uses prepared DB helpers in migrated plugin files', function () {
$targetFiles = array(
'functions.php',

Copilot uses AI. Check for mistakes.
];

$rawPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)\s*\(/';
$preparedPattern = '/\bdb_(?:execute|fetch_row|fetch_assoc|fetch_cell)_prepared\s*\(/';

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

if ($path === false) {
throw new RuntimeException("Unable to resolve required plugin file: {$relativeFile}");
}

$contents = file_get_contents($path);

if ($contents === false) {
throw new RuntimeException("Unable to read required plugin file: {$relativeFile}");
}

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

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

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

if (preg_match($rawPattern, $line) && !preg_match($preparedPattern, $line)) {
$rawCallsOutsideComments++;
}
}

expect($rawCallsOutsideComments)->toBe(0,
"File {$relativeFile} contains raw (unprepared) DB calls"
);
}
});
});
44 changes: 44 additions & 0 deletions tests/Security/SetupStructureTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?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('maint setup.php structure', function () {
$setupPath = realpath(__DIR__ . '/../../setup.php');
expect($setupPath)->not->toBeFalse();

$source = file_get_contents($setupPath);
expect($source)->not->toBeFalse();

$infoPath = realpath(__DIR__ . '/../../INFO');
expect($infoPath)->not->toBeFalse();

$info = parse_ini_file($infoPath, true);
expect($info)->not->toBeFalse();

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

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

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

it('declares a name in INFO', function () use ($info) {
expect($info['info'])->toHaveKey('name');
});

it('declares a version in INFO', function () use ($info) {
expect($info['info'])->toHaveKey('version');
});
});
Loading
Loading