mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-31 09:07:38 +09:00
* fix: match spine's unconditional engine-time preference for SNMP uptime recache (develop) PR #7982 changed cacti_snmp_select_uptime() to prefer whichever of sysUpTime and snmpEngineTime is numerically larger. Spine's own reindex-assert re-check (poller.c) has no such magnitude comparison - it always prefers a numeric engine time, only falling back to sysUpTime when the engine OID isn't present. For any device where snmpEngineTime is legitimately smaller than sysUpTime (e.g. the SNMP agent/engine restarted more recently than the OS, a common, unremarkable scenario), lib/poller.php's update_reindex_cache() now picks sysUpTime as the recache baseline while spine always re-derives its live comparison value from the smaller engine time. Every poll then reports "RECACHE ASSERT FAILED" and queues a reindex; Cacti recomputes the baseline the same way, so it never converges - an infinite RECACHE ASSERT loop that looks like a spine bug even though spine is unchanged. Fix: add an opt-in $prefer_engine_time parameter to cacti_snmp_select_uptime() that keeps the existing wall-clock rejection (issue#7342) but skips the "prefer whichever is larger" comparison, matching spine's rule exactly. Use it for the two call sites that must stay consistent with spine's own live re-check: lib/poller.php's recache baseline, and both uptime reads in cmd.php (update_system_mibs()'s host.snmp_sysUpTimeInstance update, and ping_and_reindex_check()'s own PHP-poller equivalent of spine's reindex re-check). The display-only call sites (lib/api_device.php, lib/api_automation.php) keep the existing wrap-aware "prefer larger" logic, since they aren't compared against a persisted, spine-shared baseline. Ported from the 1.2.x fix (#8057). Added tests covering the new parameter and asserting both call sites opt into it. * test(snmp): cover prefer_engine_time in the coverage-gated suite phpunit-snmp.xml's "SNMP coverage" testsuite only runs SnmpCoverageTest.php, not SnmpUptimeSelectionTest.php where the new $prefer_engine_time assertions were added. The engine-time-preference branch in cacti_snmp_select_uptime() was never executed under the 100% line coverage gate, so the "lib/snmp.php 100% coverage" check failed at 99.8%. Mirror the same prefer_engine_time assertions into SnmpCoverageTest.php so the coverage-gated suite exercises the new branch. * fix: match spine's lack of wall-clock awareness for prefer_engine_time paths The wall-clock rejection guard ran before the prefer_engine_time branch, so an OpenBSD-style agent (engine time == Unix clock) still fell back to sysUpTime on the spine-facing paths even with prefer_engine_time=true. Spine's own re-check has no wall-clock awareness at all - it unconditionally prefers any numeric engine time - so that class of device kept the RECACHE ASSERT loop. Move the prefer_engine_time short-circuit before the wall-clock check so those paths replicate spine exactly; the wall-clock rejection still protects the display-only call sites. * test(snmp): fix stale wall-clock assumption in prefer_engine_time coverage test
481 lines
22 KiB
PHP
481 lines
22 KiB
PHP
<?php
|
|
/*
|
|
+-------------------------------------------------------------------------+
|
|
| Copyright (C) 2004-2026 The Cacti Group |
|
|
| |
|
|
| This program is free software; you can redistribute it and/or |
|
|
| modify it under the terms of the GNU General Public License |
|
|
| as published by the Free Software Foundation; either version 2 |
|
|
| of the License, or (at your option) any later version. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
define('CACTI_PHP_SNMP', true);
|
|
define('CACTI_PATH_INCLUDE', dirname(__DIR__, 4) . '/include');
|
|
define('CACTI_SERVER_OS', 'unix');
|
|
define('SNMP_POLLER', 'SNMP');
|
|
define('POLLER_VERBOSITY_HIGH', 4);
|
|
|
|
$GLOBALS['snmp_coverage_config'] = [
|
|
'oid_increasing_check_disable' => 'off',
|
|
'snmp_retries' => 0,
|
|
'max_get_size' => 0,
|
|
'snmp_timeout' => 0,
|
|
'path_snmpget' => dirname(__DIR__, 3) . '/fixtures/snmp_command_probe.php',
|
|
'path_snmpgetnext' => dirname(__DIR__, 3) . '/fixtures/snmp_command_probe.php',
|
|
'path_snmpwalk' => dirname(__DIR__, 3) . '/fixtures/snmp_command_probe.php',
|
|
'path_snmpbulkwalk' => dirname(__DIR__, 3) . '/fixtures/snmp_command_probe.php'
|
|
];
|
|
$GLOBALS['snmp_coverage_logs'] = [];
|
|
$GLOBALS['snmp_coverage_debug'] = [];
|
|
$GLOBALS['snmp_priv_protocols'] = ['AES' => 'AES'];
|
|
$GLOBALS['snmp_auth_protocols'] = ['SHA' => 'SHA'];
|
|
|
|
if (!function_exists(__NAMESPACE__ . '\\read_config_option') && !function_exists('\\read_config_option')) {
|
|
function read_config_option(string $name) : mixed {
|
|
return $GLOBALS['snmp_coverage_config'][$name] ?? '';
|
|
}
|
|
}
|
|
|
|
if (!function_exists(__NAMESPACE__ . '\\cacti_sizeof') && !function_exists('\\cacti_sizeof')) {
|
|
function cacti_sizeof(mixed $value) : int {
|
|
return is_array($value) ? count($value) : 0;
|
|
}
|
|
}
|
|
|
|
if (!function_exists(__NAMESPACE__ . '\\cacti_log') && !function_exists('\\cacti_log')) {
|
|
function cacti_log(string $message, bool $output, string $environ = '', int $level = 0) : bool {
|
|
$GLOBALS['snmp_coverage_logs'][] = [$message, $output, $environ, $level];
|
|
|
|
return true;
|
|
}
|
|
}
|
|
|
|
function cacti_format_ipv6_colon(string $hostname) : string {
|
|
return str_contains($hostname, ':') ? '[' . trim($hostname, '[]') . ']' : $hostname;
|
|
}
|
|
|
|
function cacti_escapeshellcmd(string $command) : string {
|
|
return escapeshellcmd($command);
|
|
}
|
|
|
|
function cacti_escapeshellarg(string $argument) : string {
|
|
return escapeshellarg($argument);
|
|
}
|
|
|
|
function cacti_escapeshellarg_cmd(string $argument, bool $quote = true, bool $strip_env = false) : string {
|
|
if (CACTI_SERVER_OS == 'win32') {
|
|
$argument = str_replace(['"', '&', '|', '^', '<', '>', '(', ')'], '', $argument);
|
|
|
|
if ($strip_env) {
|
|
$argument = str_replace('%', '', $argument);
|
|
}
|
|
}
|
|
|
|
return cacti_escapeshellarg($argument, $quote);
|
|
}
|
|
|
|
function debug_log_insert(string $category, string $message) : void {
|
|
$GLOBALS['snmp_coverage_debug'][] = [$category, $message];
|
|
}
|
|
|
|
function __esc(string $message, mixed ...$args) : string {
|
|
return $args === [] ? $message : vsprintf($message, $args);
|
|
}
|
|
|
|
function exec_into_array(string $command, int &$return_code = 0) : array {
|
|
$output = [];
|
|
exec($command, $output, $return_code);
|
|
|
|
return $output;
|
|
}
|
|
|
|
function cacti_oid_numeric_format() : void {
|
|
}
|
|
|
|
function is_hex_string(string &$string) : bool {
|
|
$lower = strtolower($string);
|
|
|
|
if (str_starts_with($lower, 'hex-string:')) {
|
|
$check = trim(substr($string, strlen('hex-string:')));
|
|
} elseif (str_starts_with($lower, 'hex-')) {
|
|
$check = trim(substr($string, strlen('hex-')));
|
|
} else {
|
|
return false;
|
|
}
|
|
|
|
if (preg_match('/^(?:[0-9A-Fa-f]{2} ){1,}[0-9A-Fa-f]{2}$/', $check) !== 1) {
|
|
return false;
|
|
}
|
|
|
|
$string = $check;
|
|
|
|
return true;
|
|
}
|
|
|
|
function is_ipaddress(string $value) : bool {
|
|
if (!empty($GLOBALS['snmp_coverage_reject_ip'])) {
|
|
return false;
|
|
}
|
|
|
|
return filter_var($value, FILTER_VALIDATE_IP) !== false;
|
|
}
|
|
|
|
function is_mac_address(string $value) : bool {
|
|
return preg_match('/^(?:[0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/', $value) === 1;
|
|
}
|
|
|
|
function cacti_strtolower(string $value) : string {
|
|
return strtolower($value);
|
|
}
|
|
|
|
require_once dirname(__DIR__, 4) . '/lib/snmp.php';
|
|
|
|
test('uptime selection rejects wall-clock engine times and preserves wrap handling', function () : void {
|
|
$now = 1784363931;
|
|
|
|
expect(cacti_snmp_select_uptime(3015, $now, $now))->toBe(3015)
|
|
->and(cacti_snmp_select_uptime(3015, 'U', $now))->toBe(3015)
|
|
->and(cacti_snmp_select_uptime(250000, 50000000, $now))->toBe(5000000000)
|
|
->and(cacti_snmp_select_uptime(4000000, 600, $now))->toBe(4000000)
|
|
->and(cacti_snmp_select_uptime(false, 600, $now))->toBe(60000)
|
|
->and(cacti_snmp_select_uptime('U', 'U', $now))->toBeFalse();
|
|
});
|
|
|
|
test('prefer_engine_time forces the spine-compatible engine-OID preference', function () : void {
|
|
$now = 1784363931;
|
|
|
|
// spine (poller.c) always prefers a numeric engine time over sysUpTime with no
|
|
// magnitude comparison and no wall-clock awareness of its own; the recache
|
|
// baseline must use the exact same rule
|
|
expect(cacti_snmp_select_uptime(999999999, 600, $now, true))->toBe(60000)
|
|
->and(cacti_snmp_select_uptime(4000000, 600, $now, true))->toBe(60000)
|
|
->and(cacti_snmp_select_uptime(false, 600, $now, true))->toBe(60000)
|
|
// spine has no wall-clock rejection either, so an OpenBSD-style engine time
|
|
// that looks like the Unix clock must still be used here, not rejected
|
|
->and(cacti_snmp_select_uptime(3015, $now, $now, true))->toBe($now * 100)
|
|
->and(cacti_snmp_select_uptime('U', 'U', $now, true))->toBeFalse();
|
|
});
|
|
|
|
final class CoverageSnmpSession {
|
|
public array $info = ['timeout' => 1500, 'hostname' => 'coverage-host'];
|
|
public int $bulk_walk_size = 5;
|
|
public int $value_output_format = SNMP_STRING_OUTPUT_GUESS;
|
|
public mixed $result = false;
|
|
public int $errno = 9;
|
|
public string $error = '';
|
|
public bool $throw = false;
|
|
public bool $warn = false;
|
|
|
|
public function walk(mixed ...$args) : mixed {
|
|
return $this->respond();
|
|
}
|
|
|
|
public function get(mixed ...$args) : mixed {
|
|
return $this->respond();
|
|
}
|
|
|
|
public function getnext(mixed ...$args) : mixed {
|
|
return $this->respond();
|
|
}
|
|
|
|
public function notice() : bool {
|
|
trigger_error('delegated notice', E_USER_NOTICE);
|
|
|
|
return true;
|
|
}
|
|
|
|
public function getErrno() : int {
|
|
return $this->errno;
|
|
}
|
|
|
|
public function getError() : string {
|
|
return $this->error;
|
|
}
|
|
|
|
private function respond() : mixed {
|
|
if ($this->warn) {
|
|
trigger_error('transport warning', E_USER_WARNING);
|
|
}
|
|
|
|
if ($this->throw) {
|
|
throw new RuntimeException('operation exception');
|
|
}
|
|
|
|
return $this->result;
|
|
}
|
|
}
|
|
|
|
beforeEach(function () : void {
|
|
$GLOBALS['snmp_coverage_logs'] = [];
|
|
$GLOBALS['snmp_coverage_debug'] = [];
|
|
$GLOBALS['snmp_coverage_config']['oid_increasing_check_disable'] = 'off';
|
|
$GLOBALS['snmp_coverage_config']['path_snmpbulkwalk'] = dirname(__DIR__, 3) . '/fixtures/snmp_command_probe.php';
|
|
$GLOBALS['snmp_coverage_reject_ip'] = false;
|
|
unset($_SESSION);
|
|
putenv('CACTI_SNMP_PROBE_MODE=get');
|
|
});
|
|
|
|
test('session wrappers capture diagnostics and normalize native results', function () : void {
|
|
$session = new CoverageSnmpSession();
|
|
|
|
expect(cacti_snmp_session_walk($session, []))->toBe([])
|
|
->and(cacti_snmp_session_get($session, []))->toBe([])
|
|
->and(cacti_snmp_session_getnext($session, []))->toBe([]);
|
|
|
|
$session->result = ['.1' => ['STRING: one', 'INTEGER: 2'], '.2' => false, '.3' => 'STRING: three'];
|
|
expect(cacti_snmp_session_walk($session, [' .1 '], false, 0))->toBe(['.1' => ['one', '2'], '.2' => false, '.3' => 'three']);
|
|
|
|
$session->result = [];
|
|
expect(cacti_snmp_session_walk($session, ' .1 '))->toBe('.1');
|
|
|
|
$session->result = ['.1' => 'STRING: one'];
|
|
expect(cacti_snmp_session_get($session, [' .1 ']))->toBe(['.1' => 'one'])
|
|
->and(cacti_snmp_session_getnext($session, [' .1 ']))->toBe(['.1' => 'one']);
|
|
|
|
$session->result = 'STRING: scalar';
|
|
expect(cacti_snmp_session_get($session, '.1'))->toBe('scalar')
|
|
->and(cacti_snmp_session_getnext($session, '.1'))->toBe('scalar');
|
|
|
|
$session->result = false;
|
|
$session->warn = true;
|
|
expect(cacti_snmp_session_walk($session, '.1'))->toBe([])
|
|
->and(cacti_snmp_session_walk($session, '.1.3.6.1.2.1.47.1.1.1.1.2'))->toBe([])
|
|
->and(cacti_snmp_session_get($session, '.1'))->toBeFalse()
|
|
->and(cacti_snmp_session_getnext($session, '.1'))->toBeFalse();
|
|
|
|
$session->throw = true;
|
|
$session->warn = false;
|
|
expect(cacti_snmp_session_walk($session, '.1'))->toBe([])
|
|
->and(cacti_snmp_session_get($session, '.1'))->toBeFalse()
|
|
->and(cacti_snmp_session_getnext($session, '.1'))->toBeFalse();
|
|
});
|
|
|
|
test('session warning handler delegates non-warning errors and error logging is complete', function () : void {
|
|
$session = new CoverageSnmpSession();
|
|
$warning = '';
|
|
$delegated = [];
|
|
|
|
set_error_handler(function (int $level, string $message) use (&$delegated) : bool {
|
|
$delegated[] = [$level, $message];
|
|
|
|
return true;
|
|
});
|
|
|
|
try {
|
|
expect(cacti_snmp_session_call($session, 'notice', [], $warning))->toBeTrue();
|
|
} finally {
|
|
restore_error_handler();
|
|
}
|
|
|
|
expect(cacti_snmp_session_call($session, 'notice', [], $warning, false))->toBeTrue();
|
|
|
|
$session->errno = phpsnmp\SNMP::ERRNO_TIMEOUT;
|
|
cacti_snmp_log_session_error($session, $session->info, ['.1', '.2']);
|
|
$session->errno = 8;
|
|
$session->error = "native\r\nerror";
|
|
cacti_snmp_log_session_error($session, $session->info, '.1');
|
|
$session->error = '';
|
|
cacti_snmp_log_session_error($session, $session->info, '.1', 'warning reason');
|
|
cacti_snmp_log_session_error($session, $session->info, '.1');
|
|
|
|
expect($delegated[0][0])->toBe(E_USER_NOTICE)
|
|
->and($GLOBALS['snmp_coverage_logs'])->toHaveCount(4)
|
|
->and($GLOBALS['snmp_coverage_logs'][0][0])->toContain('Timeout (2 ms)')
|
|
->and($GLOBALS['snmp_coverage_logs'][1][0])->toContain('native error')
|
|
->and($GLOBALS['snmp_coverage_logs'][2][0])->toContain('warning reason')
|
|
->and($GLOBALS['snmp_coverage_logs'][3][0])->toContain('Error Number 8');
|
|
});
|
|
|
|
test('SNMP value formatting covers scalar, OID, hex, timetick, and rejection forms', function () : void {
|
|
$ip = format_snmp_string('Hex-STRING: C0 A8 01 01', false);
|
|
$GLOBALS['snmp_coverage_reject_ip'] = true;
|
|
$ascii = format_snmp_string('Hex-STRING: 41 42 43 44', false);
|
|
$GLOBALS['snmp_coverage_reject_ip'] = false;
|
|
|
|
expect(format_snmp_string('', false))->toBe('')
|
|
->and(format_snmp_string('INTEGER: 42', false))->toBe('42')
|
|
->and(format_snmp_string('.1 = STRING: value', true))->toBe('value')
|
|
->and(format_snmp_string('MIB::oid = STRING: value', true))->toBe('value')
|
|
->and(format_snmp_string('STRING: value', true))->toBe('value')
|
|
->and(format_snmp_string('prefix = STRING: value', true))->toBe('prefix = value')
|
|
->and(format_snmp_string('STRING: Wrong Type: corrected', false))->toBe('corrected')
|
|
->and(format_snmp_string('STRING: Wrong Type without separator', false))->toBe('Wrong Type without separator')
|
|
->and(format_snmp_string('abc123xyz', false, SNMP_STRING_OUTPUT_GUESS, true))->toBe('123')
|
|
->and(format_snmp_string('alphabetic', false, SNMP_STRING_OUTPUT_GUESS, true))->toBe('U')
|
|
->and(format_snmp_string("STRING: printable\x01", false))->toBe('printable')
|
|
->and($ip)->toBe('192.168.1.1')
|
|
->and($ascii)->toBe('ABCD')
|
|
->and(format_snmp_string('Hex-STRING: 00 01 02 03 04 05', false))->toBe('00:01:02:03:04:05')
|
|
->and(format_snmp_string('Hex-STRING: 01 02 03 04 05', false))->toBe('01 02 03 04 05')
|
|
->and(format_snmp_string('Hex: 01-02-03', false))->toBe('01:02:03')
|
|
->and(format_snmp_string('Hex: 00-01-02', false))->toBe('00:01:02')
|
|
->and(format_snmp_string('Hex: 123', false))->toBe('123')
|
|
->and(format_snmp_string('Hex: 00:11:22:33:44:55', false))->toBe('00:11:22:33:44:55')
|
|
->and(format_snmp_string('Timeticks: (123) 0:00:01.23', false))->toBe('123')
|
|
->and(format_snmp_string('End of MIB', false))->toBe('');
|
|
});
|
|
|
|
test('OID validation, escaping, method selection, options, and v3 auth cover all policies', function () : void {
|
|
expect(cacti_snmp_validate_oid('.1.3.6'))->toBeTrue()
|
|
->and(cacti_snmp_validate_oid('.'))->toBeFalse()
|
|
->and(cacti_snmp_validate_oid('1.bad'))->toBeFalse()
|
|
->and(snmp_escape_string('public'))->toBe("'public'")
|
|
->and(snmp_escape_string('a"b', 'win32'))->toBe("'ab'")
|
|
->and(snmp_escape_string('public', 'win32'))->toBe("'public'")
|
|
->and(snmp_get_method('get', 1, '', '', SNMP_STRING_OUTPUT_GUESS, false))->toBe(SNMP_METHOD_BINARY)
|
|
->and(snmp_get_method('get', 1, '', '', SNMP_STRING_OUTPUT_HEX))->toBe(SNMP_METHOD_BINARY)
|
|
->and(snmp_get_method('get', 3))->toBe(SNMP_METHOD_BINARY)
|
|
->and(snmp_get_method('walk', 1))->toBe(SNMP_METHOD_BINARY)
|
|
->and(snmp_get_method('get', 1))->toBe(SNMP_METHOD_PHP)
|
|
->and(snmp_get_method('get', 2))->toBe(SNMP_METHOD_PHP);
|
|
expect(snmp_get_method('get', 4))->toBe(SNMP_METHOD_BINARY);
|
|
|
|
$port = $timeout = $retries = $max_oids = 0;
|
|
expect(cacti_snmp_options_sanitize(1, 'public', $port, $timeout, $retries, $max_oids))->toBeTrue()
|
|
->and([$port, $timeout, $retries, $max_oids])->toBe([161, 500, 3, 10]);
|
|
|
|
$port = 161;
|
|
$timeout = 10;
|
|
$retries = 1;
|
|
$max_oids = 2;
|
|
expect(cacti_snmp_options_sanitize(0, '', $port, $timeout, $retries, $max_oids))->toBeFalse()
|
|
->and(cacti_snmp_options_sanitize(2, '', $port, $timeout, $retries, $max_oids))->toBeFalse()
|
|
->and(cacti_snmp_options_sanitize(3, '', $port, $timeout, $retries, $max_oids))->toBeTrue();
|
|
|
|
expect(cacti_get_snmpv3_auth('[None]', 'user', '', '[None]', '', '', ''))->toContain('noAuthNoPriv')
|
|
->and(cacti_get_snmpv3_auth('SHA', 'user', 'secret', '[None]', '', '', ''))->toContain('authNoPriv')
|
|
->and(cacti_get_snmpv3_auth('SHA', 'user', 'secret', 'AES', 'private', 'ctx', 'engine'))->toContain('authPriv')
|
|
->toContain('-n')
|
|
->toContain('-e')
|
|
->and(cacti_get_snmpv3_auth('invalid', 'user', 'secret', '[None]', '', '', ''))->toBe('')
|
|
->and(cacti_get_snmpv3_auth('SHA', 'user', 'secret', 'invalid', 'private', '', ''))->toBe('')
|
|
->and(cacti_get_snmpv3_auth('invalid', 'user', 'secret', 'AES', 'private', '', ''))->toBe('');
|
|
});
|
|
|
|
test('native sessions cover versions and security levels without network I/O', function () : void {
|
|
$GLOBALS['snmp_coverage_config']['oid_increasing_check_disable'] = 'on';
|
|
|
|
expect(cacti_snmp_session('127.0.0.1', 'public', '1'))->toBeObject()
|
|
->and(cacti_snmp_session('127.0.0.1', 'public', '2'))->toBeObject()
|
|
->and(cacti_snmp_session('127.0.0.1', '', '3', 'user', '', '[None]', '', '[None]'))->toBeObject()
|
|
->and(cacti_snmp_session('127.0.0.1', '', '3', 'user', 'secretpass', 'SHA', '', '[None]'))->toBeObject()
|
|
->and(cacti_snmp_session('127.0.0.1', '', '3', 'user', 'secretpass', 'SHA', 'privatepass', 'AES'))->toBeObject()
|
|
->and(cacti_snmp_session('127.0.0.1', '', '3', 'user', 'secretpass', 'INVALID', '', '[None]'))->toBeFalse()
|
|
->and(cacti_snmp_session('127.0.0.1', 'public', 'invalid'))->toBeFalse();
|
|
|
|
// cacti_snmp_session_from_host maps a device row onto the arguments above (#7835).
|
|
expect(cacti_snmp_session_from_host([
|
|
'hostname' => '127.0.0.1', 'snmp_community' => 'public', 'snmp_version' => '2',
|
|
]))->toBeObject()
|
|
->and(cacti_snmp_session_from_host([
|
|
'hostname' => '127.0.0.1', 'snmp_version' => '3', 'snmp_username' => 'user',
|
|
'snmp_password' => 'secretpass', 'snmp_auth_protocol' => 'SHA',
|
|
'snmp_priv_passphrase' => 'privatepass', 'snmp_priv_protocol' => 'AES',
|
|
]))->toBeObject()
|
|
->and(cacti_snmp_session_from_host(['hostname' => '127.0.0.1', 'snmp_community' => 'public', 'snmp_version' => 'invalid']))->toBeFalse();
|
|
});
|
|
|
|
test('native and binary get operations cover success and failure results', function () : void {
|
|
$host = getenv('CACTI_SNMP_COVERAGE_HOST') ?: '127.0.0.1';
|
|
$port = (int) (getenv('CACTI_SNMP_COVERAGE_PORT') ?: 21161);
|
|
$oid = '.1.3.6.1.2.1.1.1.0';
|
|
|
|
expect(cacti_snmp_get($host, 'public', $oid, 1, port: $port, timeout_ms: 500, retries: 0))->not->toBe('U')
|
|
->and(cacti_snmp_get($host, 'public', $oid, 2, port: $port, timeout_ms: 500, retries: 0))->not->toBe('U')
|
|
->and(cacti_snmp_get_raw($host, 'public', $oid, 1, port: $port, timeout_ms: 500, retries: 0))->not->toBe('U')
|
|
->and(cacti_snmp_get_raw($host, 'public', $oid, 2, port: $port, timeout_ms: 500, retries: 0))->not->toBe('U')
|
|
->and(cacti_snmp_getnext($host, 'public', '.1.3.6.1.2.1.1', 1, port: $port, timeout_ms: 500, retries: 0))->not->toBe('U')
|
|
->and(cacti_snmp_getnext($host, 'public', '.1.3.6.1.2.1.1', 2, port: $port, timeout_ms: 500, retries: 0))->not->toBe('U');
|
|
set_error_handler(static fn () : bool => true, E_WARNING);
|
|
|
|
try {
|
|
$raw_failure = cacti_snmp_get_raw($host, 'wrong', $oid, 2, port: $port, timeout_ms: 1, retries: 1);
|
|
$getnext_failure = cacti_snmp_getnext($host, 'wrong', $oid, 1, port: $port, timeout_ms: 1, retries: 1);
|
|
} finally {
|
|
restore_error_handler();
|
|
}
|
|
|
|
expect(cacti_snmp_get($host, 'public', $oid, 1, port: $port, native_get: fn () => false))->toBe('U')
|
|
->and($raw_failure)->toBe('U')
|
|
->and($getnext_failure)->toBe('U');
|
|
expect(cacti_snmp_get($host, 'public', $oid, 1, port: $port, native_get: function () : never {
|
|
throw new RuntimeException('native failure');
|
|
}))->toBe('U');
|
|
|
|
$_SESSION = [];
|
|
expect(cacti_snmp_get($host, 'public', $oid, 3, 'user', '', '[None]', '', '[None]', port: $port))->toBe('coverage value')
|
|
->and(cacti_snmp_get_raw($host, 'public', $oid, 3, 'user', '', '[None]', '', '[None]', port: $port))->toContain('coverage value')
|
|
->and(cacti_snmp_getnext($host, 'public', $oid, 3, 'user', '', '[None]', '', '[None]', port: $port))->toBe('coverage value');
|
|
|
|
putenv('CACTI_SNMP_PROBE_MODE=timeout');
|
|
expect(cacti_snmp_get($host, 'public', $oid, 1, port: $port, value_output_format: SNMP_STRING_OUTPUT_HEX))->toBe('U')
|
|
->and(cacti_snmp_get($host, 'public', $oid, 2, port: $port, value_output_format: SNMP_STRING_OUTPUT_HEX))->toBe('U')
|
|
->and(cacti_snmp_get_raw($host, 'public', $oid, 1, port: $port, value_output_format: SNMP_STRING_OUTPUT_HEX))->toBe('U')
|
|
->and(cacti_snmp_get_raw($host, 'public', $oid, 2, port: $port, value_output_format: SNMP_STRING_OUTPUT_HEX))->toBe('U');
|
|
|
|
putenv('CACTI_SNMP_PROBE_MODE=error');
|
|
expect(cacti_snmp_getnext($host, 'public', $oid, 1, port: $port, value_output_format: SNMP_STRING_OUTPUT_HEX))->toBe('probe failure')
|
|
->and(cacti_snmp_getnext($host, 'public', $oid, 2, port: $port, value_output_format: SNMP_STRING_OUTPUT_HEX))->toBe('probe failure');
|
|
|
|
$port = $timeout = $retries = $max_oids = 0;
|
|
expect(cacti_snmp_get($host, '', $oid, 1, port: $port))->toBe('U')
|
|
->and(cacti_snmp_get_raw($host, '', $oid, 1, port: $port))->toBe('U')
|
|
->and(cacti_snmp_getnext($host, '', $oid, 1, port: $port))->toBe('U')
|
|
->and(cacti_snmp_get($host, 'public', $oid, 4, port: $port))->toBe('U')
|
|
->and(cacti_snmp_get_raw($host, 'public', $oid, 4, port: $port))->toBe('U')
|
|
->and(cacti_snmp_getnext($host, 'public', $oid, 4, port: $port))->toBe('U');
|
|
});
|
|
|
|
test('native and binary walks cover parsing, filtering, and diagnostics', function () : void {
|
|
$host = getenv('CACTI_SNMP_COVERAGE_HOST') ?: '127.0.0.1';
|
|
$port = (int) (getenv('CACTI_SNMP_COVERAGE_PORT') ?: 21161);
|
|
$oid = '.1.3.6.1.2.1.1';
|
|
|
|
$GLOBALS['snmp_coverage_config']['path_snmpbulkwalk'] = '/missing/snmpbulkwalk';
|
|
expect(cacti_snmp_walk($host, 'public', $oid, 1, port: $port, timeout_ms: 500, retries: 1))->not->toBe([])
|
|
->and(cacti_snmp_walk($host, 'public', $oid, 2, port: $port, timeout_ms: 500, retries: 1))->not->toBe([]);
|
|
$GLOBALS['banned_snmp_strings'][] = 'Linux';
|
|
cacti_snmp_walk($host, 'public', $oid, 1, port: $port, timeout_ms: 500, retries: 1);
|
|
array_pop($GLOBALS['banned_snmp_strings']);
|
|
|
|
$GLOBALS['snmp_coverage_config']['path_snmpbulkwalk'] = dirname(__DIR__, 3) . '/fixtures/snmp_command_probe.php';
|
|
$GLOBALS['snmp_coverage_config']['oid_increasing_check_disable'] = 'on';
|
|
putenv('CACTI_SNMP_PROBE_MODE=walk');
|
|
$_SESSION = [];
|
|
$bulk = cacti_snmp_walk($host, 'public', $oid, 2, port: $port, bulk_walk_size: 10);
|
|
|
|
$GLOBALS['snmp_coverage_config']['path_snmpbulkwalk'] = '/missing/snmpbulkwalk';
|
|
$regular = cacti_snmp_walk($host, 'public', $oid, 1, port: $port, value_output_format: SNMP_STRING_OUTPUT_HEX);
|
|
|
|
putenv('CACTI_SNMP_PROBE_MODE=timeout');
|
|
$timeout = cacti_snmp_walk($host, 'public', $oid, 3, 'user', '', '[None]', '', '[None]', port: $port);
|
|
putenv('CACTI_SNMP_PROBE_MODE=tooBig');
|
|
$too_big = cacti_snmp_walk($host, 'public', $oid, 3, 'user', '', '[None]', '', '[None]', port: $port);
|
|
|
|
putenv('CACTI_SNMP_PROBE_MODE=walk');
|
|
$invalid = cacti_snmp_walk($host, '', $oid, 1, port: $port);
|
|
$GLOBALS['snmp_coverage_config']['oid_increasing_check_disable'] = 'off';
|
|
$without_oid_check = cacti_snmp_walk($host, 'public', $oid, 3, 'user', '', '[None]', '', '[None]', port: $port);
|
|
|
|
// An auth protocol outside $snmp_auth_protocols makes cacti_get_snmpv3_auth()
|
|
// return '', which used to build a credential-less snmpwalk -v 3.
|
|
$no_credentials = cacti_snmp_walk($host, 'public', $oid, 3, 'user', 'secret', 'BOGUS', '', '[None]', port: $port);
|
|
|
|
expect($bulk[0]['value'])->toContain('coverage agent')
|
|
->and($regular[0]['value'])->toContain('coverage agent')
|
|
->and($timeout)->toBe([])
|
|
->and($too_big)->toBe([])
|
|
->and($without_oid_check)->not->toBe([])
|
|
->and($no_credentials)->toBe([])
|
|
->and($invalid)->toBe([])
|
|
->and(implode(' ', array_column($GLOBALS['snmp_coverage_logs'], 0)))->toContain('exploit attempted')
|
|
// A failed walk is now reported by its exit code. The previous wording
|
|
// came from matching 'Timeout' against the walk output, which only ever
|
|
// matched device data.
|
|
->toContain('Exit Code')
|
|
->toContain('Missing credentials');
|
|
});
|