Files
cacti/tests/Unit/Snmp/SnmpWalkFailureDetectionTest.php
Thomas Vincent ed5ab24474 fix: detect a failed SNMP walk by exit code, not by device data (#7929)
* fix: detect a failed SNMP walk by exit code, not by device data

cacti_snmp_walk() discarded the entire walk when its output contained the word
Timeout. That output is the walk itself, one line per OID, so a device with an
ifAlias of 'Timeout Monitor' or a matching sysDescr emptied the result and
data_query.php then dropped every index for the device.

The check could not detect a real timeout either. net-snmp writes that to
stderr and neither walk command redirects it, so the matched string was always
device data. cacti_snmp_get() already reads the exit code for exactly this
reason; exec_into_array() exposes it by reference.

Also refuse a walk with empty credentials, matching cacti_snmp_get, get_raw and
getnext. An unknown v3 protocol previously built a credential-less snmpwalk and
failed with no log line.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: cover the credential guard and refresh the security baselines

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: give the exec_into_array stub the real signature

The stub dropped the by-reference exit code that lib/functions.php exposes, so
any code reading it saw 0 regardless of what the command returned. That is why
the walk's failure detection could regress to matching stdout without a test
noticing.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: expect the exit-code wording for a failed walk

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-09-02 16:36:43 -07:00

47 lines
1.9 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
$snmpSource = file_get_contents(dirname(__DIR__, 3) . '/lib/snmp.php');
function cacti_snmp_walk_body(string $source) : string {
$start = strpos($source, 'function cacti_snmp_walk(');
$end = strpos($source, "\nfunction ", $start + 1);
return substr($source, $start, $end - $start);
}
test('a failed walk is detected by the exit code, not by scanning device data', function () use ($snmpSource) {
$body = cacti_snmp_walk_body($snmpSource);
// net-snmp writes a timeout to stderr, which exec() does not capture, so
// matching 'Timeout' against walk output only ever matched device values
// such as an ifAlias of 'Timeout Monitor' and discarded the whole walk.
expect($body)->not->toContain("str_contains(implode(' ', \$temp_array), 'Timeout')")
->and($body)->toContain('$return_code != 0')
->and($body)->toContain('exec_into_array($command, $return_code)');
});
test('the walk refuses to run without credentials, as its siblings do', function () use ($snmpSource) {
expect(cacti_snmp_walk_body($snmpSource))->toContain('empty($snmp_auth)');
});
test('device values containing Timeout survive a healthy walk', function () {
// The guard the fix removes, applied to a healthy walk.
$rows = [
'.1.3.6.1.2.1.31.1.1.1.18.1 = STRING: Uplink to core',
'.1.3.6.1.2.1.31.1.1.1.18.2 = STRING: Timeout Monitor probe',
];
$discardedByOldGuard = str_contains(implode(' ', $rows), 'Timeout');
$discardedByExitCode = (0 != 0);
expect($discardedByOldGuard)->toBeTrue()
->and($discardedByExitCode)->toBeFalse();
});