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
18 changes: 18 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "cacti/plugin_evidence",
"description": "plugin_evidence 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"
]
}
}
9 changes: 4 additions & 5 deletions evidence_tab.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,9 @@ function evidence_display_form() {

if (cacti_sizeof($scan_dates)) {
foreach ($scan_dates as $scan_date) {
print '<option value="' . $scan_date . '" ' .
print '<option value="' . html_escape($scan_date) . '" ' .
(get_request_var('scan_date') == $scan_date ? ' selected="selected"' : '') .
'>' . $scan_date . '</option>';
'>' . html_escape($scan_date) . '</option>';
}
}

Expand All @@ -142,7 +142,7 @@ function evidence_display_form() {
print '</td>';
print '<td>';

print '<input type="text" name="find_text" id="find" value="' . get_request_var('find_text') . '">';
print '<input type="text" name="find_text" id="find" value="' . html_escape(get_request_var('find_text')) . '">';
print '</td>';
print '<td>';
print __('Specify data type');
Expand All @@ -157,7 +157,7 @@ function evidence_display_form() {
print '<option value="opt" ' . (get_request_var('datatype') == 'opt' ? 'selected="selected"' : '') . '>' . __('Vendor optional', 'evidence') . '</option>';

foreach ($entities as $key => $value) {
print '<option value="' . $key . '" ' . (get_request_var('datatype') == $key ? 'selected="selected"' : '') . '>Entity - ' . $value . '</option>';
print '<option value="' . html_escape($key) . '" ' . (get_request_var('datatype') == $key ? 'selected="selected"' : '') . '>Entity - ' . html_escape($value) . '</option>';
}

print '</select>';
Expand Down Expand Up @@ -260,4 +260,3 @@ function evidence_stats() {
print 'Oldest record: ' . $old . '<br/>';
}


26 changes: 6 additions & 20 deletions setup.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,26 +42,12 @@ function plugin_evidence_install () {


function plugin_evidence_uninstall () {

if (sizeof(db_fetch_assoc("SHOW TABLES LIKE 'plugin_evidence_specific_query'")) > 0 ) {
db_execute("DROP TABLE `plugin_evidence_specific_query`");
}

if (sizeof(db_fetch_assoc("SHOW TABLES LIKE 'plugin_evidence_organization'")) > 0 ) {
db_execute("DROP TABLE `plugin_evidence_organization`");
}

if (sizeof(db_fetch_assoc("SHOW TABLES LIKE 'plugin_evidence_entity'")) > 0 ) {
db_execute("DROP TABLE `plugin_evidence_entity`");
}

if (sizeof(db_fetch_assoc("SHOW TABLES LIKE 'plugin_evidence_mac'")) > 0 ) {
db_execute("DROP TABLE `plugin_evidence_mac`");
}

if (sizeof(db_fetch_assoc("SHOW TABLES LIKE 'plugin_evidence_vendor_specific'")) > 0 ) {
db_execute("DROP TABLE `plugin_evidence_vendor_specific`");
}
db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_specific_query`', []);
db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_organization`', []);
db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_entity`', []);
db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_mac`', []);
db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_ip`', []);
db_execute_prepared('DROP TABLE IF EXISTS `plugin_evidence_vendor_specific`', []);
}


Expand Down
17 changes: 17 additions & 0 deletions tests/E2E/EvidenceFilterXssRegressionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

describe('evidence filter xss regression wiring', function () {
it('keeps raw request variables out of the filter value attribute', function () {
$contents = file_get_contents(realpath(__DIR__ . '/../../evidence_tab.php'));

expect($contents)->not->toContain('value="' . "' . get_request_var('find_text') . '");
expect($contents)->toContain('value="' . "' . html_escape(get_request_var('find_text')) . '");
});
});
23 changes: 23 additions & 0 deletions tests/Integration/EvidenceTabEscapingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

describe('evidence tab integration escaping', function () {
it('escapes scan date values rendered into select options', function () {
$contents = file_get_contents(realpath(__DIR__ . '/../../evidence_tab.php'));

expect($contents)->toContain('html_escape($scan_date)');
});

it('escapes entity keys and values rendered into select options', function () {
$contents = file_get_contents(realpath(__DIR__ . '/../../evidence_tab.php'));

expect($contents)->toContain('html_escape($key)');
expect($contents)->toContain('html_escape($value)');
});
});
10 changes: 10 additions & 0 deletions tests/Pest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

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

describe('auth guard presence in evidence', function () {
it('includes auth.php or global.php in all UI entry points', function () {
$uiFiles = array(
'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;

// Files that include setup.php or are library files don't need direct auth
if (strpos($relativeFile, 'include/') === 0 || strpos($relativeFile, 'lib/') === 0) continue;
if (strpos($relativeFile, 'poller_') === 0) continue;

$hasAuth = (
strpos($contents, 'auth.php') !== false ||
strpos($contents, 'global.php') !== false ||
strpos($contents, 'global_arrays.php') !== false
);

expect($hasAuth)->toBeTrue(
"File {$relativeFile} does not include auth.php or global.php"
);
}
});

it('validates numeric IDs from request variables before DB queries', function () {
$uiFiles = array(
'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"
);
}
}
});
});
76 changes: 76 additions & 0 deletions tests/Security/OutputEscapingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

describe('output escaping in evidence', function () {
it('does not interpolate raw variables into HTML attributes', function () {
$uiFiles = array(
'evidence_tab.php',
);

foreach ($uiFiles as $relativeFile) {
$path = realpath(__DIR__ . '/../../' . $relativeFile);
if ($path === false) continue;
$contents = file_get_contents($path);
if ($contents === false) continue;

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

foreach ($lines as $line) {
$trimmed = ltrim($line);
if (strpos($trimmed, '//') === 0 || strpos($trimmed, '*') === 0) continue;

// value="$row[...] without html_escape wrapping
if (preg_match('/value\s*=\s*["\'"]\s*<\?php\s+echo\s+\$/', $line)) {
$dangerous++;
}
// title="<?php print $something without escaping
if (preg_match('/(?:title|alt|placeholder)\s*=.*print\s+\$(?!_|config)/', $line)) {
if (strpos($line, 'html_escape') === false && strpos($line, '__esc') === false && strpos($line, 'htmlspecialchars') === false) {
$dangerous++;
}
}
}

expect($dangerous)->toBe(0,
"File {$relativeFile} has unescaped variables in HTML attributes"
);
}
});

it('uses html_escape or __esc for user-controlled output', function () {
$uiFiles = array(
'evidence_tab.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'
);
});

it('escapes the evidence filter text before rendering it into the HTML value attribute', function () {
$contents = file_get_contents(realpath(__DIR__ . '/../../evidence_tab.php'));

expect($contents)->toContain(
'html_escape(get_request_var(\'find_text\'))'
);
});
});
113 changes: 113 additions & 0 deletions tests/Security/Php74CompatibilityTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/

describe('PHP 7.4 compatibility in evidence', function () {
$files = array(
'setup.php',
'tests/test_prepared_statements.php',
);

it('does not use str_contains (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_contains\s*\(/', $c))->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();
});
});
Loading