From f12373e800b1b8d25b269090de513cdd102f8f5e Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Thu, 22 Oct 2026 20:50:42 -0600 Subject: [PATCH 1/2] fix: use prepared statements for device-reported values The interface, interface-graph, ARP, DHCP snooping and 802.1x scanners now bind the strings a device reports instead of building them into the SQL. Signed-off-by: Thomas Vincent --- CHANGELOG.md | 1 + lib/mactrack_aruba_oscx.php | 27 +- lib/mactrack_cisco.php | 75 ++--- lib/mactrack_enterasys_N7.php | 27 +- lib/mactrack_extreme.php | 27 +- lib/mactrack_functions.php | 120 +++---- lib/mactrack_h3c_3com.php | 27 +- phpunit.xml | 5 + .../Unit/DeviceReportedValuesBindingTest.php | 292 ++++++++++++++++++ tests/bootstrap-unit.php | 66 ++++ 10 files changed, 529 insertions(+), 138 deletions(-) create mode 100644 tests/Unit/DeviceReportedValuesBindingTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ad5743d..962cadf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ * issue: Pin the CI database image and shared test dependencies, disable persisted checkout credentials, and limit workflow permissions and runtime * issue: Reach PHPStan level 8 with zero errors across all first-party PHP and add native function return type declarations, using no baselines or inline suppressions * issue: Use prepared statements for all vendor collection +* issue: Use prepared statements for device-reported values in the interface, ARP, DHCP snooping and 802.1x scanners * issue: Fix a variable-variable typo that broke 3Com interface-graph mac updates * issue: Return the rendered device-row HTML from the enable/disable AJAX handlers, which previously captured an always-null value * issue: Decode MAC/IP OIDs as hex in the Extreme scanner by correcting a misplaced xform_stripped_oid() argument diff --git a/lib/mactrack_aruba_oscx.php b/lib/mactrack_aruba_oscx.php index 08b37068..ef0db86f 100644 --- a/lib/mactrack_aruba_oscx.php +++ b/lib/mactrack_aruba_oscx.php @@ -517,24 +517,27 @@ function get_aruba_oscx_arp_table($site, &$device): void { // output details to database if (cacti_sizeof($result)) { - $sql = []; + $sql = []; + $params = []; foreach ($result as $key=>$value) { - $sql[] = '(' . - $device['site_id'] . ', ' . - $device['device_id'] . ', ' . - db_qstr($device['hostname']) . ', ' . - db_qstr($device['device_name']) . ', ' . - db_qstr($value['port']) . ', ' . - db_qstr($value['mac']) . ', ' . - db_qstr($key) . ', ' . - db_qstr($scan_date) . ')'; + $sql[] = '(?, ?, ?, ?, ?, ?, ?, ?)'; + + array_push($params, + $device['site_id'], + $device['device_id'], + $device['hostname'], + $device['device_name'], + $value['port'], + $value['mac'], + $key, + $scan_date); } if (cacti_sizeof($sql)) { - db_execute('REPLACE INTO mac_track_ips + db_execute_prepared('REPLACE INTO mac_track_ips (site_id, device_id, hostname, device_name, port_number, mac_address,ip_address,scan_date) - VALUES ' . implode(', ', $sql)); + VALUES ' . implode(', ', $sql), $params); } } diff --git a/lib/mactrack_cisco.php b/lib/mactrack_cisco.php index bd0e6f4e..afe662a0 100644 --- a/lib/mactrack_cisco.php +++ b/lib/mactrack_cisco.php @@ -764,20 +764,20 @@ function get_cisco_dhcpsnooping_table($site, &$device): void { if (cacti_sizeof($cdsBindingEntries)) { foreach ($cdsBindingEntries as $cdsBindingEntry) { if ($cdsBindingEntry['cdsBindingsIpAddress'] != '') { // It's acceptable to have IPs without a MAC (meaning that MAC is not present but DHCP entry is still present) here but not the other way around. - $insert_string = 'REPLACE INTO mac_track_ips + db_execute_prepared('REPLACE INTO mac_track_ips (site_id,device_id,hostname,device_name,port_number, mac_address,ip_address,scan_date) - VALUES (' . - $device['site_id'] . ',' . - $device['device_id'] . ',' . - db_qstr($device['hostname']) . ',' . - db_qstr($device['device_name']) . ',' . - db_qstr($cdsBindingEntry['cdsBindingIndex']) . ',' . - db_qstr($cdsBindingEntry['dot1dTpFdbEntry']) . ',' . - db_qstr($cdsBindingEntry['cdsBindingsIpAddress']) . ',' . - db_qstr($scan_date) . ')'; - - db_execute($insert_string); + VALUES (?, ?, ?, ?, ?, ?, ?, ?)', + [ + $device['site_id'], + $device['device_id'], + $device['hostname'], + $device['device_name'], + $cdsBindingEntry['cdsBindingIndex'], + $cdsBindingEntry['dot1dTpFdbEntry'], + $cdsBindingEntry['cdsBindingsIpAddress'], + $scan_date + ]); } } } @@ -812,20 +812,21 @@ function get_cisco_vrf_arp_table($site, &$device): void { // output details to database if (cacti_sizeof($cdsBindingEntries)) { foreach ($cdsBindingEntries as $cdsBindingEntry) { - $insert_string = 'INSERT INTO mac_track_arp + db_execute_prepared('INSERT INTO mac_track_arp (site_id,ip_address,mac_address,scan_date) VALUES - (' . - $device['site_id'] . ',' . - db_qstr($cdsBindingEntry['key']) . ',' . - db_qstr($cdsBindingEntry['value']) . ',' . - db_qstr($scan_date) . ' - ) + (?, ?, ?, ?) ON DUPLICATE KEY UPDATE - `ip_address` = ' . db_qstr($cdsBindingEntry['key']) . ', - `scan_date` = ' . db_qstr($scan_date) . ';'; - - db_execute($insert_string); + `ip_address` = ?, + `scan_date` = ?', + [ + $device['site_id'], + $cdsBindingEntry['key'], + $cdsBindingEntry['value'], + $scan_date, + $cdsBindingEntry['key'], + $scan_date + ]); } } @@ -901,6 +902,7 @@ function get_cisco_dot1x_table($site, &$device): void { // output details to database $sql = []; + $params = []; $prefix = 'REPLACE INTO mac_track_dot1x (site_id, device_id, hostname, device_name, username, mac_address, ip_address, domain, status, port_number, scan_date) VALUES '; @@ -908,22 +910,25 @@ function get_cisco_dot1x_table($site, &$device): void { if (cacti_sizeof($entries)) { foreach ($entries as $entry) { if ($entry['UserName'] != '') { - $sql[] = '(' . $device['site_id'] . ',' . - $device['device_id'] . ',' . - db_qstr($device['hostname']) . ',' . - db_qstr($device['device_name']) . ',' . - db_qstr($entry['UserName']) . ',' . - db_qstr($entry['cafSessionClientMacAddress']) . ',' . - db_qstr($entry['cafSessionClientAddress']) . ',' . - db_qstr($entry['cafSessionDomain']) . ',' . - db_qstr($entry['cafSessionStatus']) . ',' . - db_qstr($entry['port_number']) . ',' . - db_qstr($scan_date) . ')'; + $sql[] = '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'; + + array_push($params, + $device['site_id'], + $device['device_id'], + $device['hostname'], + $device['device_name'], + $entry['UserName'], + $entry['cafSessionClientMacAddress'], + $entry['cafSessionClientAddress'], + $entry['cafSessionDomain'], + $entry['cafSessionStatus'], + $entry['port_number'], + $scan_date); } } if (cacti_sizeof($sql)) { - db_execute($prefix . implode(', ', $sql)); + db_execute_prepared($prefix . implode(', ', $sql), $params); } } } diff --git a/lib/mactrack_enterasys_N7.php b/lib/mactrack_enterasys_N7.php index 22e69ccf..b1cb1e72 100644 --- a/lib/mactrack_enterasys_N7.php +++ b/lib/mactrack_enterasys_N7.php @@ -535,7 +535,8 @@ function get_CTAlias_table($site, &$device): void { // output details to database if (cacti_sizeof($CTAliasEntries)) { - $sql = []; + $sql = []; + $params = []; foreach ($CTAliasEntries as $CTAliasEntry) { // drop non-IP protocols @@ -543,20 +544,22 @@ function get_CTAlias_table($site, &$device): void { continue; } - $sql[] = '(' . - $device['site_id'] . ', ' . - $device['device_id'] . ', ' . - db_qstr($device['hostname']) . ', ' . - db_qstr($device['device_name']) . ', ' . - db_qstr($CTAliasEntry['ifIndex']) . ', ' . - db_qstr($CTAliasEntry['CTAliasMacAddress']) . ', ' . - db_qstr($CTAliasEntry['CTAliasAddressText']) . ', ' . - db_qstr($scan_date) . ')'; + $sql[] = '(?, ?, ?, ?, ?, ?, ?, ?)'; + + array_push($params, + $device['site_id'], + $device['device_id'], + $device['hostname'], + $device['device_name'], + $CTAliasEntry['ifIndex'], + $CTAliasEntry['CTAliasMacAddress'], + $CTAliasEntry['CTAliasAddressText'], + $scan_date); } - db_execute('REPLACE INTO mac_track_ips + db_execute_prepared('REPLACE INTO mac_track_ips (site_id,device_id,hostname,device_name,port_number, mac_address,ip_address,scan_date) - VALUES ' . implode(', ', $sql)); + VALUES ' . implode(', ', $sql), $params); } // save ip information for the device diff --git a/lib/mactrack_extreme.php b/lib/mactrack_extreme.php index 517fea0b..4a3fafd1 100644 --- a/lib/mactrack_extreme.php +++ b/lib/mactrack_extreme.php @@ -348,24 +348,27 @@ function get_extreme_arp_table($site, &$device, $extremeware = false): void { // output details to database if (cacti_sizeof($atEntries)) { - $sql = []; + $sql = []; + $params = []; foreach ($atEntries as $atEntry) { - $sql[] = '(' . - $device['site_id'] . ', ' . - $device['device_id'] . ', ' . - db_qstr($device['hostname']) . ', ' . - db_qstr($device['device_name']) . ', ' . - db_qstr($atEntry['atifIndex']) . ', ' . - db_qstr($atEntry['atPhysAddress']) . ', ' . - db_qstr($atEntry['atNetAddress']) . ', ' . - db_qstr($scan_date) . ')'; + $sql[] = '(?, ?, ?, ?, ?, ?, ?, ?)'; + + array_push($params, + $device['site_id'], + $device['device_id'], + $device['hostname'], + $device['device_name'], + $atEntry['atifIndex'], + $atEntry['atPhysAddress'], + $atEntry['atNetAddress'], + $scan_date); } if (cacti_sizeof($sql)) { - db_execute('REPLACE INTO mac_track_ips + db_execute_prepared('REPLACE INTO mac_track_ips (site_id, device_id, hostname, device_name, port_number, mac_address,ip_address,scan_date) - VALUES ' . implode(', ', $sql)); + VALUES ' . implode(', ', $sql), $params); } } diff --git a/lib/mactrack_functions.php b/lib/mactrack_functions.php index 5381fce5..0c7afe91 100644 --- a/lib/mactrack_functions.php +++ b/lib/mactrack_functions.php @@ -766,7 +766,8 @@ function build_InterfacesTable(&$device, &$ifIndexes, $getLinkPorts = false, $ge stateChanges=VALUES(stateChanges), present="1"'; - $insert_vals = ''; + $insert_vals = ''; + $insert_params = []; $ifSpeed = xform_standard_indexed_data('.1.3.6.1.2.1.2.2.1.5', $device); mactrack_debug("ifSpeed data collection complete. '" . cacti_sizeof($ifSpeed) . "' rows found!"); @@ -1057,46 +1058,51 @@ function build_InterfacesTable(&$device, &$ifIndexes, $getLinkPorts = false, $ge $mac_address = isset($ifPhysAddress[$ifIndex]) ? xform_mac_address($ifPhysAddress[$ifIndex]) : ''; - $insert_vals .= "('" . - $device['site_id'] . "', '" . $device['device_id'] . "', '" . - $device['snmp_sysUptime'] . "', '" . $ifIndex . "', " . - db_qstr($type) . ', ' . db_qstr($name) . ', ' . - db_qstr($ifAlias) . ", '" . $linkPort . "', '" . - $vlan_id . "', " . db_qstr($vlan_name) . ", '" . - $vlan_trunk . "', '" . $speed . "', '" . - (isset($ifHighSpeed[$ifIndex]) ? $ifHighSpeed[$ifIndex] : '') . "', '" . - (isset($ifDuplex[$ifIndex]) ? $ifDuplex[$ifIndex] : '') . "', " . - db_qstr($desc) . ", '" . - (isset($ifMtu[$ifIndex]) ? $ifMtu[$ifIndex] : '') . "', " . - db_qstr($mac_address) . ", '" . - (isset($ifAdminStatus[$ifIndex]) ? $ifAdminStatus[$ifIndex] : '') . "', '" . - (isset($ifOperStatus[$ifIndex]) ? $ifOperStatus[$ifIndex] : '') . "', '" . - (isset($ifLastChange[$ifIndex]) ? $ifLastChange[$ifIndex] : '') . "', '" . - (isset($ifInOctets[$ifIndex]) ? $ifInOctets[$ifIndex] : '') . "', '" . - (isset($ifOutOctets[$ifIndex]) ? $ifOutOctets[$ifIndex] : '') . "', '" . - (isset($ifHCInOctets[$ifIndex]) ? $ifHCInOctets[$ifIndex] : '') . "', '" . - (isset($ifHCOutOctets[$ifIndex]) ? $ifHCOutOctets[$ifIndex] : '') . "', '" . - (isset($ifInUcastPkts[$ifIndex]) ? $ifInUcastPkts[$ifIndex] : '') . "', '" . - (isset($ifOutUcastPkts[$ifIndex]) ? $ifOutUcastPkts[$ifIndex] : '') . "', '" . - (isset($ifInDiscards[$ifIndex]) ? $ifInDiscards[$ifIndex] : '') . "', '" . - (isset($ifInErrors[$ifIndex]) ? $ifInErrors[$ifIndex] : '') . "', '" . - (isset($ifInUnknownProtos[$ifIndex]) ? $ifInUnknownProtos[$ifIndex] : '') . "', '" . - (isset($ifOutDiscards[$ifIndex]) ? $ifOutDiscards[$ifIndex] : '') . "', '" . - (isset($ifOutErrors[$ifIndex]) ? $ifOutErrors[$ifIndex] : '') . "', '" . - (isset($ifInMulticastPkts[$ifIndex]) ? $ifInMulticastPkts[$ifIndex] : '') . "', '" . - (isset($ifOutMulticastPkts[$ifIndex]) ? $ifOutMulticastPkts[$ifIndex] : '') . "', '" . - (isset($ifInBroadcastPkts[$ifIndex]) ? $ifInBroadcastPkts[$ifIndex] : '') . "', '" . - (isset($ifOutBroadcastPkts[$ifIndex]) ? $ifOutBroadcastPkts[$ifIndex] : '') . "', '" . - @$int_ifInOctets . "', '" . @$int_ifOutOctets . "', '" . - @$int_ifHCInOctets . "', '" . @$int_ifHCOutOctets . "', '" . - @$int_ifInMulticastPkts . "', '" . @$int_ifOutMulticastPkts . "', '" . - @$int_ifInBroadcastPkts . "', '" . @$int_ifOutBroadcastPkts . "', '" . - @$int_ifInUcastPkts . "', '" . @$int_ifOutUcastPkts . "', '" . - @$int_ifInDiscards . "', '" . @$int_ifInErrors . "', '" . - @$int_ifInUnknownProtos . "', '" . @$int_ifOutDiscards . "', '" . - @$int_ifOutErrors . "', '" . @$int_discards_present . "', '" . - $int_errors_present . "', '" . $last_down_time . "', '" . - $last_up_time . "', '" . $stateChanges . "', '" . "1')"; + $insert_row = [ + $device['site_id'], $device['device_id'], + $device['snmp_sysUptime'] ?? '', $ifIndex, + $type, $name, + $ifAlias, $linkPort, + $vlan_id, $vlan_name, + $vlan_trunk, $speed, + (isset($ifHighSpeed[$ifIndex]) ? $ifHighSpeed[$ifIndex] : ''), + (isset($ifDuplex[$ifIndex]) ? $ifDuplex[$ifIndex] : ''), + $desc, + (isset($ifMtu[$ifIndex]) ? $ifMtu[$ifIndex] : ''), + $mac_address, + (isset($ifAdminStatus[$ifIndex]) ? $ifAdminStatus[$ifIndex] : ''), + (isset($ifOperStatus[$ifIndex]) ? $ifOperStatus[$ifIndex] : ''), + (isset($ifLastChange[$ifIndex]) ? $ifLastChange[$ifIndex] : ''), + (isset($ifInOctets[$ifIndex]) ? $ifInOctets[$ifIndex] : ''), + (isset($ifOutOctets[$ifIndex]) ? $ifOutOctets[$ifIndex] : ''), + (isset($ifHCInOctets[$ifIndex]) ? $ifHCInOctets[$ifIndex] : ''), + (isset($ifHCOutOctets[$ifIndex]) ? $ifHCOutOctets[$ifIndex] : ''), + (isset($ifInUcastPkts[$ifIndex]) ? $ifInUcastPkts[$ifIndex] : ''), + (isset($ifOutUcastPkts[$ifIndex]) ? $ifOutUcastPkts[$ifIndex] : ''), + (isset($ifInDiscards[$ifIndex]) ? $ifInDiscards[$ifIndex] : ''), + (isset($ifInErrors[$ifIndex]) ? $ifInErrors[$ifIndex] : ''), + (isset($ifInUnknownProtos[$ifIndex]) ? $ifInUnknownProtos[$ifIndex] : ''), + (isset($ifOutDiscards[$ifIndex]) ? $ifOutDiscards[$ifIndex] : ''), + (isset($ifOutErrors[$ifIndex]) ? $ifOutErrors[$ifIndex] : ''), + (isset($ifInMulticastPkts[$ifIndex]) ? $ifInMulticastPkts[$ifIndex] : ''), + (isset($ifOutMulticastPkts[$ifIndex]) ? $ifOutMulticastPkts[$ifIndex] : ''), + (isset($ifInBroadcastPkts[$ifIndex]) ? $ifInBroadcastPkts[$ifIndex] : ''), + (isset($ifOutBroadcastPkts[$ifIndex]) ? $ifOutBroadcastPkts[$ifIndex] : ''), + @$int_ifInOctets, @$int_ifOutOctets, + @$int_ifHCInOctets, @$int_ifHCOutOctets, + @$int_ifInMulticastPkts, @$int_ifOutMulticastPkts, + @$int_ifInBroadcastPkts, @$int_ifOutBroadcastPkts, + @$int_ifInUcastPkts, @$int_ifOutUcastPkts, + @$int_ifInDiscards, @$int_ifInErrors, + @$int_ifInUnknownProtos, @$int_ifOutDiscards, + @$int_ifOutErrors, @$int_discards_present, + $int_errors_present, $last_down_time, + $last_up_time, $stateChanges, '1' + ]; + + $insert_vals .= '(' . implode(', ', array_fill(0, cacti_sizeof($insert_row), '?')) . ')'; + + array_push($insert_params, ...$insert_row); $i++; } @@ -1105,7 +1111,7 @@ function build_InterfacesTable(&$device, &$ifIndexes, $getLinkPorts = false, $ge if ($insert_vals != '') { // add/update records in the database - db_execute($insert_prefix . $insert_vals . $insert_suffix); + db_execute_prepared($insert_prefix . $insert_vals . $insert_suffix, $insert_params); // remove all obsolete records from the database db_execute_prepared('DELETE FROM mac_track_interfaces @@ -1217,8 +1223,9 @@ function mactrack_find_host_graphs($device_id, $host_id): void { } } - $sql = ''; - $found = 0; + $sql = ''; + $params = []; + $found = 0; if (cacti_sizeof($output_array)) { $interfaces = array_rekey( @@ -1232,15 +1239,18 @@ function mactrack_find_host_graphs($device_id, $host_id): void { foreach ($interfaces as $key => $data) { if (isset($output_array[$key])) { foreach ($output_array[$key] as $local_graph_id => $graph_details) { - $sql .= ($sql != '' ? ', (' : '(') . - $data['ifIndex'] . ",'" . - $key . "'," . - $local_graph_id . ',' . - $device_id . ',' . - $host_id . ',' . - $graph_details[0] . ',' . - $graph_details[1] . ",'" . - $key . "','" . $field_name . "', 1)"; + $sql .= ($sql != '' ? ', ' : '') . '(?, ?, ?, ?, ?, ?, ?, ?, ?, 1)'; + + array_push($params, + $data['ifIndex'], + $key, + $local_graph_id, + $device_id, + $host_id, + $graph_details[0], + $graph_details[1], + $key, + $field_name); $found++; } @@ -1257,7 +1267,7 @@ function mactrack_find_host_graphs($device_id, $host_id): void { AND host_id = ?', [$device_id, $host_id]); - db_execute("INSERT INTO mac_track_interface_graphs + db_execute_prepared("INSERT INTO mac_track_interface_graphs (ifIndex, ifName, local_graph_id, device_id, host_id, snmp_query_id, graph_template_id, field_value, field_name, present) VALUES $sql ON DUPLICATE KEY UPDATE @@ -1265,7 +1275,7 @@ function mactrack_find_host_graphs($device_id, $host_id): void { graph_template_id=VALUES(graph_template_id), field_value=VALUES(field_value), field_name=VALUES(field_name), - present=VALUES(present)"); + present=VALUES(present)", $params); db_execute_prepared('DELETE FROM mac_track_interface_graphs WHERE present = 0 diff --git a/lib/mactrack_h3c_3com.php b/lib/mactrack_h3c_3com.php index 3fad6a9d..ad041383 100644 --- a/lib/mactrack_h3c_3com.php +++ b/lib/mactrack_h3c_3com.php @@ -495,24 +495,27 @@ function get_h3c_3com_arp_table($site, &$device): void { // output details to database if (cacti_sizeof($atEntries)) { - $sql = []; + $sql = []; + $params = []; foreach ($atEntries as $atEntry) { - $sql[] = '(' . - $device['site_id'] . ', ' . - $device['device_id'] . ', ' . - db_qstr($device['hostname']) . ', ' . - db_qstr($device['device_name']) . ', ' . - db_qstr($atEntry['atifIndex']) . ', ' . - db_qstr($atEntry['atPhysAddress']) . ', ' . - db_qstr($atEntry['atNetAddress']) . ', ' . - db_qstr($scan_date) . ')'; + $sql[] = '(?, ?, ?, ?, ?, ?, ?, ?)'; + + array_push($params, + $device['site_id'], + $device['device_id'], + $device['hostname'], + $device['device_name'], + $atEntry['atifIndex'], + $atEntry['atPhysAddress'], + $atEntry['atNetAddress'], + $scan_date); } if (cacti_sizeof($sql)) { - db_execute('REPLACE INTO mac_track_ips + db_execute_prepared('REPLACE INTO mac_track_ips (site_id, device_id, hostname, device_name, port_number, mac_address,ip_address,scan_date) - VALUES ' . implode(', ', $sql)); + VALUES ' . implode(', ', $sql), $params); } } diff --git a/phpunit.xml b/phpunit.xml index ac7e0e96..e9479d95 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -31,6 +31,11 @@ includes/database.php lib/mactrack_functions.php lib/mactrack_dns_resolution.php + lib/mactrack_aruba_oscx.php + lib/mactrack_cisco.php + lib/mactrack_enterasys_N7.php + lib/mactrack_extreme.php + lib/mactrack_h3c_3com.php diff --git a/tests/Unit/DeviceReportedValuesBindingTest.php b/tests/Unit/DeviceReportedValuesBindingTest.php new file mode 100644 index 00000000..73fc6ae5 --- /dev/null +++ b/tests/Unit/DeviceReportedValuesBindingTest.php @@ -0,0 +1,292 @@ +device = [ + 'site_id' => 4, + 'device_id' => 9, + 'host_id' => 0, + 'hostname' => 'sw-lab-01', + 'device_name' => 'Lab switch', + 'snmp_readstring' => 'public', + 'snmp_version' => '2', + 'snmp_username' => '', + 'snmp_password' => '', + 'snmp_auth_protocol' => '', + 'snmp_priv_passphrase' => '', + 'snmp_priv_protocol' => '', + 'snmp_context' => '', + 'snmp_port' => 161, + 'snmp_timeout' => 500, + 'snmp_retries' => 1, + 'max_oids' => 10, + 'snmp_engine_id' => '', + 'snmp_sysUptime' => '123456', + 'snmp_sysDescr' => 'test agent', + 'last_rundate' => '', + ]; +}); + +/** + * Canned walk rows for one OID, keyed by the index that follows it. + * + * @param string $oid Base OID as the scanner passes it. + * @param array $rows index => value + * + * @return void + */ +function mactrack_test_walk($oid, $rows) { + foreach ($rows as $index => $value) { + $GLOBALS['__test_snmp_walk'][$oid][] = ['oid' => $oid . '.' . $index, 'value' => $value]; + } +} + +/** + * The single recorded call whose SQL starts with $prefix. + * + * @param string $prefix Leading SQL keywords, e.g. 'REPLACE INTO mac_track_ips'. + * + * @return array + */ +function mactrack_test_find_call($prefix) { + $found = []; + + foreach ($GLOBALS['__test_db_calls'] as $call) { + if (strpos(ltrim($call['sql']), $prefix) === 0) { + $found[] = $call; + } + } + + expect($found)->toHaveCount(1); + + return $found[0]; +} + +/** + * Assert the call is prepared, binds every value, and carries none of them in its text. + * + * @param array $call Recorded stub call. + * @param array $values Device-reported values that must be bound. + * + * @return void + */ +function mactrack_test_expect_bound($call, $values) { + expect($call['fn'])->toBe('db_execute_prepared'); + expect(substr_count($call['sql'], '?'))->toBe(count($call['params'])); + + foreach ($values as $value) { + expect($call['params'])->toContain($value); + expect($call['sql'])->not->toContain($value); + } +} + +test('interface rows bind the names, aliases, descriptions and MAC the device reports', function () { + mactrack_test_walk('.1.3.6.1.2.1.2.2.1.1', ['5' => '5']); + mactrack_test_walk('.1.3.6.1.2.1.2.2.1.3', ['5' => '6']); + mactrack_test_walk('.1.3.6.1.2.1.31.1.1.1.1', ['5' => 'Gi1/0/5-lab']); + mactrack_test_walk('.1.3.6.1.2.1.31.1.1.1.18', ['5' => 'printer room uplink']); + mactrack_test_walk('.1.3.6.1.2.1.2.2.1.2', ['5' => 'GigabitEthernet1/0/5 descr']); + mactrack_test_walk('.1.3.6.1.2.1.2.2.1.6', ['5' => '00:11:22:33:44:55']); + mactrack_test_walk('.1.3.6.1.2.1.2.2.1.7', ['5' => 'up(1)']); + mactrack_test_walk('.1.3.6.1.2.1.2.2.1.8', ['5' => 'down(2)']); + + $ifIndexes = []; + + build_InterfacesTable($this->device, $ifIndexes, false, true); + + $call = mactrack_test_find_call('INSERT INTO mac_track_interfaces'); + + mactrack_test_expect_bound($call, ['Gi1/0/5-lab', 'printer room uplink', 'GigabitEthernet1/0/5 descr', '001122334455']); + + expect($call['params'])->toHaveCount(56); + expect(array_slice($call['params'], 0, 7))->toBe([4, 9, '123456', '5', '6', 'Gi1/0/5-lab', 'printer room uplink']); + expect($call['params'][14])->toBe('GigabitEthernet1/0/5 descr'); + expect($call['params'][16])->toBe('001122334455'); + expect($call['params'][17])->toBe(1); + expect($call['params'][18])->toBe(0); + expect($call['params'][55])->toBe('1'); +}); + +test('a device with no stored uptime binds an empty sysUptime, not NULL', function () { + mactrack_test_walk('.1.3.6.1.2.1.2.2.1.1', ['5' => '5']); + + $this->device['snmp_sysUptime'] = null; + $ifIndexes = []; + + build_InterfacesTable($this->device, $ifIndexes, false, false); + + $call = mactrack_test_find_call('INSERT INTO mac_track_interfaces'); + + expect($call['params'][2])->toBe(''); + expect($call['params'])->not->toContain(null); +}); + +test('interface graph links bind the interface name read back from the device', function () { + mactrack_test_queue_return('db_fetch_assoc_prepared', [ + ['id' => 31, 'field_value' => 'Gi1/0/5-lab', 'snmp_query_id' => 2], + ]); + mactrack_test_queue_return('db_fetch_assoc_prepared', [ + ['id' => 41, 'graph_template_id' => 7], + ]); + mactrack_test_queue_return('db_fetch_assoc', [ + ['device_id' => 9, 'ifIndex' => 5, 'ifName' => 'Gi1/0/5-lab'], + ]); + + mactrack_find_host_graphs(9, 12); + + $call = mactrack_test_find_call('INSERT INTO mac_track_interface_graphs'); + + mactrack_test_expect_bound($call, ['Gi1/0/5-lab']); + + expect($call['params'])->toBe([5, 'Gi1/0/5-lab', 41, 9, 12, 7, 2, 'Gi1/0/5-lab', 'ifName']); +}); + +test('Cisco DHCP snooping rows bind the port, MAC and IP the device reports', function () { + mactrack_test_walk('.1.3.6.1.4.1.9.9.380.1.4.1.1.5', ['10.0.17.34.51.68.85' => 'Gi2/0/7']); + mactrack_test_walk('.1.3.6.1.4.1.9.9.380.1.4.1.1.4', ['10.0.17.34.51.68.85' => '192.0.2.10']); + mactrack_test_walk('.1.3.6.1.4.1.9.9.46.1.3.1.1.2', ['10' => '1']); + mactrack_test_walk('.1.3.6.1.4.1.9.9.46.1.3.1.1.4', ['10' => 'users']); + mactrack_test_walk('.1.3.6.1.2.1.17.4.3.1.1', ['0.17.34.51.68.85' => '00 11 22 33 44 55']); + mactrack_test_queue_return('cacti_snmp_get', '3'); + + // xform_stripped_oid() strips the OID without its leading dot. + foreach ($GLOBALS['__test_snmp_walk'] as $oid => $rows) { + foreach ($rows as $i => $row) { + $GLOBALS['__test_snmp_walk'][$oid][$i]['oid'] = ltrim($row['oid'], '.'); + } + } + + get_cisco_dhcpsnooping_table([], $this->device); + + $call = mactrack_test_find_call('REPLACE INTO mac_track_ips'); + + mactrack_test_expect_bound($call, ['Gi2/0/7', '001122334455', '192.0.2.10', 'sw-lab-01', 'Lab switch']); + + expect($call['params'])->toBe([4, 9, 'sw-lab-01', 'Lab switch', 'Gi2/0/7', '001122334455', '192.0.2.10', '2026-10-22 10:00:00']); +}); + +test('Cisco VRF ARP rows bind the IP and MAC the device reports', function () { + mactrack_test_walk('.1.3.6.1.2.1.3.1.1.2', ['12.1.192.0.2.20' => '0 11 22 33 44 66']); + + get_cisco_vrf_arp_table([], $this->device); + + $call = mactrack_test_find_call('INSERT INTO mac_track_arp'); + + mactrack_test_expect_bound($call, ['192.0.2.20', '0:11:22:33:44:66']); + + expect($call['params'])->toBe([4, '192.0.2.20', '0:11:22:33:44:66', '2026-10-22 10:00:00', '192.0.2.20', '2026-10-22 10:00:00']); +}); + +test('Cisco 802.1x rows bind the user name, domain and addresses the device reports', function () { + $base = '1.3.6.1.4.1.9.9.656.1.4.1.1'; + + foreach (['10' => 'lab-user-17', '2' => '00:11:22:33:44:77', '4' => '192.0.2.30', '6' => 'data', '5' => 'authorized'] as $column => $value) { + $GLOBALS['__test_snmp_walk']['.' . $base . '.' . $column][] = ['oid' => $base . '.' . $column . '.10101.4', 'value' => $value]; + } + + get_cisco_dot1x_table([], $this->device); + + $call = mactrack_test_find_call('REPLACE INTO mac_track_dot1x'); + + mactrack_test_expect_bound($call, ['lab-user-17', '001122334477', '192.0.2.30', 'authorized']); + + expect($call['params'])->toBe([4, 9, 'sw-lab-01', 'Lab switch', 'lab-user-17', '001122334477', '192.0.2.30', 'data', 'authorized', '10101', '2026-10-22 10:00:00']); +}); + +test('Aruba AOS-CX ARP rows bind the port, MAC and IP the device reports', function () { + mactrack_test_walk('.1.3.6.1.2.1.17.7.1.2.2.1.2', ['1.0.17.34.51.68.85' => '28']); + mactrack_test_walk('.1.3.6.1.2.1.4.35.1.4', ['16777415.1.4.192.0.2.40' => '00 11 22 33 44 55']); + + get_aruba_oscx_arp_table([], $this->device); + + $call = mactrack_test_find_call('REPLACE INTO mac_track_ips'); + + mactrack_test_expect_bound($call, ['00:11:22:33:44:55', '192.0.2.40']); + + expect($call['params'])->toBe([4, 9, 'sw-lab-01', 'Lab switch', '28', '00:11:22:33:44:55', '192.0.2.40', '2026-10-22 10:00:00']); +}); + +test('Enterasys N7 alias rows bind the MAC and IP text the device reports', function () { + mactrack_test_walk('.1.3.6.1.4.1.52.4.1.3.7.1.1.1.1.3', ['100.1' => '7']); + mactrack_test_walk('.1.3.6.1.4.1.52.4.1.3.7.1.1.1.1.4', ['100.1' => '00:11:22:33:44:88']); + mactrack_test_walk('.1.3.6.1.4.1.52.4.1.3.7.1.1.1.1.6', ['100.1' => '1']); + mactrack_test_walk('.1.3.6.1.4.1.52.4.1.3.7.1.1.1.1.9', ['100.1' => '192.0.2.50']); + + get_CTAlias_table([], $this->device); + + $call = mactrack_test_find_call('REPLACE INTO mac_track_ips'); + + mactrack_test_expect_bound($call, ['001122334488', '192.0.2.50']); + + expect($call['params'])->toBe([4, 9, 'sw-lab-01', 'Lab switch', '7', '001122334488', '192.0.2.50', '2026-10-22 10:00:00']); +}); + +test('H3C/3Com ARP rows bind the interface, MAC and IP the device reports', function () { + mactrack_test_walk('.1.3.6.1.2.1.3.1.1.1', ['9.1.192.0.2.60' => 'Vlan-interface60']); + mactrack_test_walk('.1.3.6.1.2.1.4.22.1.2', ['9.192.0.2.60' => '00:11:22:33:44:99']); + mactrack_test_walk('.1.3.6.1.2.1.3.1.1.3', ['9.1.192.0.2.60' => '192.0.2.60']); + + get_h3c_3com_arp_table([], $this->device); + + $call = mactrack_test_find_call('REPLACE INTO mac_track_ips'); + + mactrack_test_expect_bound($call, ['Vlan-interface60', '001122334499', '192.0.2.60']); + + expect($call['params'])->toBe([4, 9, 'sw-lab-01', 'Lab switch', 'Vlan-interface60', '001122334499', '192.0.2.60', '2026-10-22 10:00:00']); +}); + +test('Extreme ARP rows bind the interface description, MAC and IP the device reports', function () { + $walks = [ + '.1.3.6.1.2.1.3.1.1.1' => ['9.1.192.0.2.70' => '9'], + '.1.3.6.1.2.1.3.1.1.2' => ['9.1.192.0.2.70' => '00:11:22:33:44:aa'], + '.1.3.6.1.2.1.3.1.1.3' => ['9.1.192.0.2.70' => 'C0 00 02 46'], + '.1.3.6.1.2.1.2.2.1.2' => ['9' => 'vlan70 lab uplink'], + ]; + + foreach ($walks as $oid => $rows) { + foreach ($rows as $index => $value) { + $GLOBALS['__test_snmp_walk'][$oid][] = ['oid' => ltrim($oid, '.') . '.' . $index, 'value' => $value]; + } + } + + get_extreme_arp_table([], $this->device, true); + + $call = mactrack_test_find_call('REPLACE INTO mac_track_ips'); + + mactrack_test_expect_bound($call, ['vlan70 lab uplink', '0011223344AA', '192.0.2.70']); + + expect($call['params'])->toBe([4, 9, 'sw-lab-01', 'Lab switch', 'vlan70 lab uplink', '0011223344AA', '192.0.2.70', '2026-10-22 10:00:00']); +}); diff --git a/tests/bootstrap-unit.php b/tests/bootstrap-unit.php index 11496ee2..77c2095b 100644 --- a/tests/bootstrap-unit.php +++ b/tests/bootstrap-unit.php @@ -455,6 +455,52 @@ function sql_save($array, $table, $key = 'id') { } } +/* + * SNMP answers for the scanner functions. A test stores the rows a walk of + * an OID returns in $GLOBALS['__test_snmp_walk'][$oid], in the + * array('oid' => ..., 'value' => ...) shape cacti_snmp_walk() returns. + */ +if (!function_exists('cacti_snmp_walk')) { + function cacti_snmp_walk($hostname, $community, $oid, $version, $auth_user = '', $auth_pass = '', + $auth_proto = '', $priv_pass = '', $priv_proto = '', $context = '', + $port = 161, $timeout_ms = 500, $retries = 0, $max_oids = 10, $environ = 0, + $engineid = '', $value_output_format = 1) { + return $GLOBALS['__test_snmp_walk'][$oid] ?? array(); + } +} + +if (!function_exists('cacti_snmp_get')) { + function cacti_snmp_get($hostname, $community, $oid, $version, $auth_user = '', $auth_pass = '', + $auth_proto = '', $priv_pass = '', $priv_proto = '', $context = '', + $port = 161, $timeout_ms = 500, $retries = 0, $environ = 0, + $engineid = '', $value_output_format = 1) { + return mactrack_test_next_return('cacti_snmp_get', ''); + } +} + +// Same body as Cacti's lib/functions.php. +if (!function_exists('array_rekey')) { + function array_rekey($array, $key, $key_value) { + $ret_array = array(); + + if (is_array($array)) { + foreach ($array as $item) { + $item_key = $item[$key]; + + if (is_array($key_value)) { + foreach ($key_value as $value) { + $ret_array[$item_key][$value] = $item[$value]; + } + } else { + $ret_array[$item_key] = $item[$key_value]; + } + } + } + + return $ret_array; + } +} + if (!defined('CACTI_PATH_BASE')) { define('CACTI_PATH_BASE', $GLOBALS['config']['base_path']); } @@ -475,6 +521,26 @@ function sql_save($array, $table, $key = 'id') { define('POLLER_VERBOSITY_NONE', 6); } +if (!defined('POLLER_VERBOSITY_HIGH')) { + define('POLLER_VERBOSITY_HIGH', 4); +} + +if (!defined('HOST_UP')) { + define('HOST_UP', 3); +} + +if (!defined('SNMP_POLLER')) { + define('SNMP_POLLER', 0); +} + +if (!defined('SNMP_STRING_OUTPUT_GUESS')) { + define('SNMP_STRING_OUTPUT_GUESS', 1); +} + +if (!defined('SNMP_STRING_OUTPUT_HEX')) { + define('SNMP_STRING_OUTPUT_HEX', 3); +} + if (!defined('MESSAGE_LEVEL_ERROR')) { define('MESSAGE_LEVEL_ERROR', 1); } From a03f90f5c24c410b9d44102f0f20c0598d24b7e5 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Thu, 22 Oct 2026 20:51:50 -0600 Subject: [PATCH 2/2] fix: require POST for the admin device and site actions Rescan, site scan, enable and disable now refuse any other method with a 405, and js/mactrack.js posts them with the CSRF token and reports failures. Signed-off-by: Thomas Vincent --- CHANGELOG.md | 1 + js/mactrack.js | 23 +++- lib/mactrack_functions.php | 38 ++++++ tests/Unit/AdminActionsRequirePostTest.php | 136 +++++++++++++++++++++ tests/bootstrap-unit.php | 34 ++++++ 5 files changed, 227 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/AdminActionsRequirePostTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 962cadf8..a47882ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * security: Restrict Net_DNS2 cache deserialization to DNS response classes * security: Remove all inline JavaScript handlers like onClick, onChange * security: Add a version-safe CSP nonce (`plugin_mactrack_csp_nonce()`) to every inline `