mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-23 03:27:02 +09:00
* fix(win32): harden the cmd.exe shell sinks on develop (GHSA-rjvj-r52f-8v5q) Forward-port of the 1.2.x win32 command-injection remediation. On Windows the SNMP/ping/data-query/trap commands run through cmd.exe, which ignores the \" escape and toggles quoting on every ", so cacti_escapeshellarg cannot stop & | ^ < > ( ). - add cacti_escapeshellarg_cmd(), stripping those operators on win32 before quoting (a Unix pass-through) - snmp_escape_string(): strip the operators in the win32 branch - route the SNMP binary hostname, the data-query device values (get_script_query_path), the data-input arg builders, the Windows ping hostname, and the SNMP trap notification arguments through the strip get_script_query_path is device-reachable (a rogue monitored device supplies an SNMP index/system field that executes on the Windows poller); the rest are post-auth. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * fix(win32): strip %VAR% from the hostname on Windows (GHSA-rjvj-r52f-8v5q) cmd.exe expands %VAR% even inside quotes, so a device hostname of %USERNAME% would expand into the SNMP/ping command line. Add an opt-in $strip_env flag to cacti_escapeshellarg_cmd() and set it for the SNMP binary and ping hostnames, which never legitimately contain a percent. Community and credential values are left untouched because they can legitimately contain %. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs(changelog): note win32 cmd.exe hardening for develop * security: require Package Management permission to trust new package signers package_accept_key() (reachable via the repository-based package import flow) inserted new keys into package_public_keys, the global trust store consulted by import_validate_signature() for every future import by every user, without checking for the Package Management (realm 29) permission that gates package_keys.php/package.php/package_repos.php for the same table. An Import Templates (realm 17) user could therefore add themselves as a trusted signer for the whole install. Also add the 'accept' action to the CSRF-protected bad_actions list so the acceptance step requires POST plus a valid token like every other state-changing action. GHSA-gxgm-8rhv-r7jv * docs(changelog): note package-signer trust authorization fix * security: close tab-based open redirect bypass in CSRF timeout handler sanitize_uri()'s guard (20298c6cd) models the WHATWG rule that browsers strip leading C0 controls/whitespace before resolving a redirect, but Chromium separately strips an embedded tab from anywhere in the target. A request target of /%09/evil.example passes the guard (no leading control char, not two leading slashes) but Chromium reduces the resulting 'Location: /<TAB>/evil.example' header to a network-path reference and navigates off-site. 1.2.x already uses validate_redirect_url() for this same sink (3338a63e1), which resolves the URL via parse_url() and checks host/port against the server-configured values, so it isn't bypassable this way. Switch develop's CSRF timeout handler to match. GHSA-c725-4jm5-9fx5 * security: authorize single-item Report actions against ownership reports.php sets \ = true, so include/auth.php grants an implicit guest session (when the Guest User feature is enabled) without ever evaluating the realm 22 gate declared for this page. The single-item action handlers reports_item_remove()/_moveup()/_movedown() relied solely on that page-level gate and performed no independent ownership check, so an unauthenticated guest-bound request could delete or reorder reports_items rows belonging to any report, including ones owned by an administrator. reports_form_actions() already gained an equivalent \ check; add the same authorization (Reports admin realm 21, or the requesting user owns the item's report) to the three single-item handlers. Same underlying chain as the 1.2.31 fix for reports_user.php (guest bind removed + cacti_authorize_resource() added to reports_item_remove()); the rewritten develop reports.php/lib/html_reports.php did not carry either half of that fix forward. GHSA-j5jh-fhwg-r56x * security: authorize tree node CRUD actions against per-tree access tree.php dispatches create_node/delete_node/move_node/rename_node/copy_node directly to api_tree_*() with no is_tree_allowed() check, even though the normal tree-view path already enforces that boundary. A non-admin user with the Trees realm but no explicit grant on a given tree_id could still create, delete, move, rename or copy nodes on that tree by calling these actions directly, bypassing the authorization boundary enforced elsewhere. GHSA-prxj-84qq-v5jq * security: remove shell from Realtime RRD create and validate DS bounds poller_realtime.php built the Realtime-graph RRD 'create' command line (including the SNMP-sourced ifSpeed/ifHighSpeed interface speed, substituted in verbatim as the DS maximum) and ran it through shell_exec(). A hostile or spoofed monitored device could return an ifSpeed value such as '1000000000\uid=197608(ladams) gid=197121 groups=197121' and get arbitrary OS command execution as the web/poller user the next time any user with the Realtime Graphs permission (or an unauthenticated guest, where configured) viewed a realtime graph for an affected data source. - lib/rrd.php: rrdtool_function_interface_speed() now casts ifHighSpeed/ ifSpeed to int before they become the DS maximum. - lib/rrd.php: rrdtool_function_create() now rejects any non-numeric rrd_minimum/rrd_maximum right before they are concatenated onto the rrdtool command line, forcing them to 'U' and logging instead. - poller_realtime.php: the RRD create is now routed through the same shell-free rrdtool_execute()/proc_open(bypass_shell) path every other RRDtool invocation already uses, matching the pattern 1.2.x already uses for this exact call site, instead of shell_exec(). GHSA-f88m-q789-g22j * security: authorize Report save/edit against report ownership reports_form_save()'s update path fetched the existing report's owner from the database but never compared it to the session user before calling sql_save(), so any user holding only the Reports realm could overwrite another user's report by POSTing its id. reports_edit() had the same gap on the read side: it fetched a report by id with no ownership check, so the same user could view any other user's report configuration (recipients, schedule, etc.). 1.2.x already guards both call sites with cacti_authorize_resource(..., 'reports'); develop had lost that check. GHSA-8f5c-3h59-q7gc * security: reject ORDER BY sort_column subquery bypass sanitize_sql_column()'s allowlist regex kept parentheses to support the INET_ATON(col)/NATURAL_SORT_KEY(col) sort wrappers, but that also let MySQL subquery syntax through unchanged, since a subquery like (SELECT(0)FROM(SELECT(SLEEP(5)))a) needs only alphanumerics and parentheses. get_order_string() then detects the '(' and disables its backtick quoting, so the payload reaches ORDER BY verbatim, giving any user with realm 2-5 a blind, time-based SQL injection via sort_column. sanitize_sql_column() now only allows '(' through when the whole value is exactly one of the two known safe wrapper forms around a plain identifier; anything else containing '(' -- including subquery syntax -- now falls back to the default. Added regression tests covering the reported subquery and REGEXP-exfiltration payloads. GHSA-8j73-cvf6-jrvx * security: confine Reports format_file to a bare filename reports_form_save() wrote the POST-supplied format_file value verbatim into reports.format_file, and reports_load_format_file() concatenated it onto CACTI_PATH_FORMATS with no directory-component check, so a value such as ../../../../etc/passwd escaped the formats directory and any Reports-realm user could read arbitrary files readable by the web server. 1.2.x already fixed the same sink (GHSA-g37j-39f4-6r4j / CVE-2026-41884, GHSA-mjvw-mhj5-9jcj / CVE-2026-40084); develop had lost the guard. format_file is now confined to basename() at both the save site and the load site, with the load site additionally rejecting a bare '.'/'..'/empty result before any file IO. Added regression tests. GHSA-5x27-wqpm-m24v * security: authorize Data Source enable/disable against owning device ds_enable()/ds_disable() and the bulk drp_action enable/disable loops in form_actions() passed a user-controlled local_data_id straight into api_data_source_enable()/api_data_source_disable() with no object-level authorization check, so any user holding the Sites/Devices/Data realm could toggle the enabled state of a data source belonging to a device outside their assigned Device Permissions. Added data_source_authorized(\), which allows host-independent data sources (data_local.host_id = 0) and otherwise defers to is_device_allowed(\), and applied it to both the single-item and bulk action paths. GHSA-2fhv-c794-v433 * security: authorize bulk Change Data Source Profile against owning device The bulk 'Change Data Source Profile' action (drp_action == '6') iterated selected_items and updated data_template_data/data_template_rrd/poller_item for each supplied local_data_id with no object-level authorization check, so any Sites/Devices/Data realm user could retarget the RRD step/heartbeat of a data source belonging to a device outside their assigned Device Permissions -- the same class of gap as GHSA-2fhv, in a different bulk action. Guarded with the data_source_authorized() helper added for that fix. This action does not exist on 1.2.x (develop-only feature). GHSA-fc82-w6xq-4jg7 * security: grant AllowOverride All in docker/apache.conf The packaged docker/apache.conf granted only 'AllowOverride FileInfo Options=FollowSymLinks' for the Cacti document root. Cacti's shipped .htaccess deny rules (log/, cache/*, scripts/, cli/, bin/, mibs/, rra/, contrib/, tests/) rely on 'Require all denied' (Apache 2.4, override type AuthConfig) and/or legacy 'Order/Deny' (override type Limit), neither of which FileInfo/Options covers, so Apache silently ignored every one of those deny rules and served the directories over HTTP to any unauthenticated client -- most notably log/cacti.log, which can contain usernames, client IPs, SQL backtraces, and any secrets Cacti logs (SNMP community strings, LDAP bind details). AllowOverride All lets every shipped .htaccess apply as intended. Added a regression test. docker/ does not exist on 1.2.x (develop-only). GHSA-fxfg-j995-j69j * docs: add security advisory / CVE policy to Copilot instructions Documents the policy established for this remediation effort: develop is unreleased, so a develop-only vulnerability does not get a CVE requested; 1.2.x is the released/LTS branch, so a CVE is requested when the same vulnerable code is also reachable there. Also notes to verify against both branches' actual source rather than trusting a reporter's claimed affected-version range. * fix(security): update stale test harnesses and address review findings CI fixes: - CsrfCheckContractProbe.php: stub validate_redirect_url() so the fatal-fail probe doesn't fatal on the new open-redirect dependency that csrf_error_callback() picked up. - InstallerCsrfRecoveryTest.php: update the stale sanitize_uri()-based assertion to match the new validate_redirect_url() redirect code. - psalm-baseline.xml: drop the TaintedHeader suppression for include/csrf.php now that validate_redirect_url() resolves it. Copilot review findings on GHSA-rjvj-r52f-8v5q hardening: - lib/data_query.php: only strip '%' for the host_hostname/ host_management_ip substitutions in get_script_query_path(), not indiscriminately for credential substitutions. - lib/functions.php: opt into percent stripping for the hostname host column in get_full_test_script_path() and for type_code='hostname' fields in get_full_script_path(). - lib/snmpagent.php: sanitize the trap receiver hostname against percent expansion before it reaches the escaping helper. - tests/security/baselines/*.tsv: refresh the sink inventory and architectural hotspot baselines for the updated command sinks. - tests/Unit/DataCollection/Snmp/SnmpCoverageTest.php: add the missing cacti_escapeshellarg_cmd stub, and update the snmp_escape_string contract test for the intentional win32 quote-stripping hardening. - tests/Unit/Security/Shell/CmdMetacharHelperTest.php + tests/fixtures/CmdMetacharHelperWin32Probe.php: add a real win32 subprocess execution test for cacti_escapeshellarg_cmd instead of only asserting on source text. - CHANGELOG: add the pull-request reference to the new entry. * fix(snmp): remove now-unreachable quote-escaping branch in snmp_escape_string The metachar strip on the line above already removes the double quote on win32, so the substr_count(SNMP_ESCAPE_CHARACTER) branch below it could never execute; drop it so lib/snmp.php reaches 100% coverage again. * style: fix pre-existing php-cs-fixer blank-line drift blocking CI data_sources.php and lib/html_reports.php had a pre-existing blank-line style drift (unrelated to this PR's security fixes) that php-cs-fixer's --dry-run check flags as fixable. Since "PHP 8.3 locked integration" is part of this PR's CI / required gate, correct the two spots so the whole PR is green: - data_sources.php: collapse a double blank line before form_actions(). - lib/html_reports.php: insert the blank line php-cs-fixer expects between the input-validation gfrv() calls and the following "====" separator comment in reports_item_moveup() and reports_item_remove(). No behavior change; purely CI/style formatting. * style: refresh phpstan baseline for pre-existing, unrelated findings Three PHPStan findings were masked behind the earlier php-cs-fixer failure and only surfaced once that step passed: - lib/html_reports.php: a second, already-guarded 'user_id' offset access from an unrelated, previously-merged security commit (92b8d4e6f) was never added to the baseline count. - cli/splice_rrd.php, poller_realtime.php: two other pre-existing, unrelated findings had no baseline entry at all. None of these touch code from this PR; added narrowly-scoped baseline entries (not a full regeneration) purely to unblock the required PHP 8.3 locked integration check. * fix(security): address follow-up review findings on consolidated PR Closes the four remaining review findings from the follow-up review round: - package_import.php: trustSigner() now POSTs with a CSRF token instead of $.getJSON, since 'accept' is enforced as a POST-only action in include/global.php's bad_actions list (GHSA-gxgm-8rhv-r7jv). - lib/html_reports.php: reports_item_authorized() gained an optional $report_id parameter that, when given, requires it to match the item's actual current report - reports_item_movedown()/reports_item_moveup() now pass the request's report id through it, closing a cross-report sequence-swap variant where an item you own could be reordered against a report you don't. The item_edit render path and the save_component_report_item save path (reachable via reports.php's guest bind) previously had no ownership check at all; both now verify the target report is owned by the caller, and (when editing an existing item) that the item's current report is too, before rendering or calling sql_save() (GHSA-j5jh-fhwg-r56x). - tree.php + include/global.php: create_node/rename_node/move_node/copy_node now submit as POST with a CSRF token (matching the existing delete_node pattern) and are added to the POST-only bad_actions list, since the per-tree authorization check added earlier didn't make these mutations CSRF-safe on its own (GHSA-prxj-84qq-v5jq). Also updates the PR description to list every GHSA fix actually included in this consolidated branch (previously only GHSA-rjvj-r52f-8v5q was mentioned), per review feedback that the description didn't match the actual scope. * fix: resolve CI failures on PR #8056 (docblock alignment, sink baseline) - lib/html_reports.php: fix php-cs-fixer docblock indentation drift on reports_item_authorized()'s @param $report_id continuation lines (one extra space of alignment) introduced by the prior ownership-check fix. - tests/security/baselines/sink_inventory.baseline.tsv: refresh to track the 4 new header('Location: ' . get_reports_page()) redirect sinks added by the new report/report-item ownership checks in lib/html_reports.php. Verified via tests/security/verify_sink_inventory.sh and confirmed tests/security/verify_architectural_hotspots.sh still reports no new hotspots. * fix: track new PHPStan offset findings from extended reports_item_authorized() phpstan-baseline.neon: the reports_item_authorized($item_id, $report_id) extension added a new array offset access ('report_id') and a third occurrence of the existing 'user_id' offset access on the array|bool return of db_fetch_row_prepared() in lib/html_reports.php. Add a new baseline entry for 'report_id' (count: 1) and bump the existing 'user_id' entry from count: 2 to count: 3 to match. Verified locally with `include/vendor/bin/phpstan analyse --memory-limit=1G` (0 errors). --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: Thomas Vincent <thomasvincent@gmail.com> Co-authored-by: TheWitness <copilot@users.noreply.github.com>
1032 lines
37 KiB
PHP
1032 lines
37 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. |
|
|
| |
|
|
| This program is distributed in the hope that it will be useful, |
|
|
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
|
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
|
| GNU General Public License for more details. |
|
|
+-------------------------------------------------------------------------+
|
|
| Cacti: The Complete RRDtool-based Graphing Solution |
|
|
+-------------------------------------------------------------------------+
|
|
| This code is designed, written, and maintained by the Cacti Group. See |
|
|
| about.php and/or the AUTHORS file for specific developer information. |
|
|
+-------------------------------------------------------------------------+
|
|
| http://www.cacti.net/ |
|
|
+-------------------------------------------------------------------------+
|
|
*/
|
|
|
|
function snmpagent_enabled() : bool {
|
|
return read_config_option('enable_snmp_agent') == 'on';
|
|
}
|
|
|
|
function snmpagent_cacti_stats_update(array $data) : bool {
|
|
$mc = new MibCache();
|
|
|
|
if (!snmpagent_enabled()) {
|
|
return false;
|
|
}
|
|
|
|
// refresh total stats
|
|
$mc->object('cactiStatsTotalsDevices')->set(snmpagent_read('cactiStatsTotalsDevices'));
|
|
$mc->object('cactiStatsTotalsDataSources')->set(snmpagent_read('cactiStatsTotalsDataSources'));
|
|
$mc->object('cactiStatsTotalsGraphs')->set(snmpagent_read('cactiStatsTotalsGraphs'));
|
|
|
|
// local polling stats - does not support distributed environments so far.
|
|
$mc->object('cactiStatsLocalPollerRuntime')->set($data[0]);
|
|
|
|
$index = 1;
|
|
$values = [
|
|
'cactiStatsPollerRunTime' => $data[0],
|
|
'cactiStatsPollerConcurrentProcesses' => $data[2],
|
|
'cactiStatsPollerThreads' => $data[3],
|
|
'cactiStatsPollerHosts' => $data[4],
|
|
'cactiStatsPollerHostsPerProcess' => $data[5],
|
|
'cactiStatsPollerItems' => $data[6],
|
|
'cactiStatsPollerRrrdsProcessed' => $data[7],
|
|
'cactiStatsPollerUtilization' => round($data[0] / read_config_option('poller_interval', true) * 100, 10)
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiStatsPollerTable')->row($index)->update($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
|
|
$mc->object('cactiStatsLastUpdate')->set(time());
|
|
|
|
return true;
|
|
}
|
|
|
|
function snmpagent_global_settings_update() : void {
|
|
$mc = new MibCache();
|
|
|
|
$mc->object('cactiApplVersion')->set(snmpagent_read('cactiApplVersion'));
|
|
$mc->object('cactiApplSnmpVersion')->set(snmpagent_read('cactiApplSnmpVersion'));
|
|
$mc->object('cactiApplRrdtoolVersion')->set(get_rrdtool_version());
|
|
$mc->object('cactiApplPollerEnabled')->set((read_config_option('poller_enabled', true) == 'on') ? 1 : 2);
|
|
$mc->object('cactiApplPollerType')->set(read_config_option('poller_type', true));
|
|
$mc->object('cactiApplPollerInterval')->set(read_config_option('poller_interval', true));
|
|
$mc->object('cactiApplPollerMaxProcesses')->set(read_config_option('concurrent_processes', true));
|
|
$mc->object('cactiApplPollerLoadBalance')->set((read_config_option('process_leveling', true) == 'on') ? 1 : 2);
|
|
$mc->object('cactiApplSpineMaxThreads')->set(read_config_option('max_threads', true));
|
|
$mc->object('cactiApplSpineScriptServers')->set(read_config_option('php_servers', true));
|
|
$mc->object('cactiApplSpineScriptTimeout')->set(read_config_option('script_timeout', true));
|
|
$mc->object('cactiApplSpineMaxOids')->set(read_config_option('max_get_size', true));
|
|
$mc->object('cactiApplLastUpdate')->set(time());
|
|
|
|
// update boost settings
|
|
$mc->mib('CACTI-BOOST-MIB');
|
|
$mc->object('boostApplRrdUpdateEnabled')->set((read_config_option('boost_rrd_update_enable', true) == 'on') ? 1 : 2);
|
|
$mc->object('boostApplRrdUpdateInterval')->set(read_config_option('boost_rrd_update_interval', true));
|
|
$mc->object('boostApplRrdUpdateMaxRecords')->set(read_config_option('boost_rrd_update_max_records', true));
|
|
$mc->object('boostApplRrdUpdateMaxRecordsPerSelect')->set(read_config_option('boost_rrd_update_max_records_per_select', true));
|
|
$mc->object('boostApplRrdUpdateMaxStringLength')->set(read_config_option('boost_rrd_update_string_length', true));
|
|
$mc->object('boostApplRrdUpdatePollerMemLimit')->set(read_config_option('boost_poller_mem_limit', true));
|
|
$mc->object('boostApplRrdUpdateMaxRunTime')->set(read_config_option('boost_rrd_update_max_runtime', true));
|
|
$mc->object('boostApplRrdUpdateRedirect')->set((read_config_option('boost_redirect', true) == 'on') ? 1 : 2);
|
|
$mc->object('boostApplImageCacheEnabled')->set((read_config_option('boost_png_cache_enable', true) == 'on') ? 1 : 2);
|
|
$mc->object('boostApplLoggingEnabled')->set((read_config_option('boost_debug_enabled', true) == 'on') ? 1 : 2);
|
|
$mc->object('boostApplLastUpdate')->set(time());
|
|
}
|
|
|
|
function snmpagent_api_device_new(array $device) : bool {
|
|
if (!snmpagent_enabled()) {
|
|
return false;
|
|
}
|
|
|
|
$mc = new MibCache();
|
|
// add device to cactiApplDeviceTable and cactiStatsDeviceTable
|
|
$device_data = db_fetch_row_prepared('SELECT * FROM `host` WHERE id = ?', [$device['id']]);
|
|
|
|
$appl_values = [
|
|
'cactiApplDeviceIndex' => $device_data['id'],
|
|
'cactiApplDeviceDescription' => $device_data['description'],
|
|
'cactiApplDeviceHostname' => $device_data['hostname'],
|
|
'cactiApplDeviceStatus' => $device_data['status'],
|
|
'cactiApplDeviceEventCount' => $device_data['status_event_count'],
|
|
'cactiApplDeviceFailDate' => $device_data['status_fail_date'],
|
|
'cactiApplDeviceRecoveryDate' => $device_data['status_rec_date'],
|
|
'cactiApplDeviceLastError' => $device_data['status_last_error'],
|
|
];
|
|
|
|
$stats_values = [
|
|
'cactiStatsDeviceIndex' => $device_data['id'],
|
|
'cactiStatsDeviceHostname' => $device_data['hostname'],
|
|
'cactiStatsDeviceMinTime' => $device_data['min_time'],
|
|
'cactiStatsDeviceMaxTime' => $device_data['max_time'],
|
|
'cactiStatsDeviceCurTime' => $device_data['cur_time'],
|
|
'cactiStatsDeviceAvgTime' => $device_data['avg_time'],
|
|
'cactiStatsDeviceTotalPolls' => $device_data['total_polls'],
|
|
'cactiStatsDeviceFailedPolls' => $device_data['failed_polls'],
|
|
'cactiStatsDeviceAvailability' => $device_data['availability']
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiApplDeviceTable')->row($device['id'])->replace($appl_values);
|
|
$mc->object('cactiApplLastUpdate')->set(time());
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
|
|
try {
|
|
$mc->table('cactiStatsDeviceTable')->row($device['id'])->replace($stats_values);
|
|
$mc->object('cactiStatsTotalsDevices')->set(snmpagent_read('cactiStatsTotalsDevices'));
|
|
$mc->object('cactiStatsLastUpdate')->set(time());
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function snmpagent_data_source_action_bottom(array $data) : bool {
|
|
if (!snmpagent_enabled()) {
|
|
return false;
|
|
}
|
|
|
|
$mc = new MibCache();
|
|
$action = $data[0];
|
|
|
|
if ($action == '1') {
|
|
// delete data sources
|
|
$mc->object('cactiStatsTotalsDataSources')->set(snmpagent_read('cactiStatsTotalsDataSources'));
|
|
$mc->object('cactiStatsTotalsGraphs')->set(snmpagent_read('cactiStatsTotalsGraphs'));
|
|
$mc->object('cactiStatsLastUpdate')->set(time());
|
|
} elseif ($action == '4') {
|
|
// duplicate data sources
|
|
$mc->object('cactiStatsTotalsDataSources')->set(snmpagent_read('cactiStatsTotalsDataSources'));
|
|
$mc->object('cactiStatsLastUpdate')->set(time());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function snmpagent_graphs_action_bottom(array $data) : bool {
|
|
if (!snmpagent_enabled()) {
|
|
return false;
|
|
}
|
|
|
|
$mc = new MibCache();
|
|
$action = $data[0];
|
|
|
|
if ($action == '1') {
|
|
// delete graphs
|
|
$mc->object('cactiStatsTotalsDataSources')->set(snmpagent_read('cactiStatsTotalsDataSources'));
|
|
$mc->object('cactiStatsTotalsGraphs')->set(snmpagent_read('cactiStatsTotalsGraphs'));
|
|
$mc->object('cactiStatsLastUpdate')->set(time());
|
|
} elseif ($action == '3') {
|
|
// duplicate graphs
|
|
$mc->object('cactiStatsTotalsGraphs')->set(snmpagent_read('cactiStatsTotalsGraphs'));
|
|
$mc->object('cactiStatsLastUpdate')->set(time());
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function snmpagent_device_action_bottom(array $data) : bool {
|
|
if (!snmpagent_enabled()) {
|
|
return false;
|
|
}
|
|
|
|
$mc = new MibCache();
|
|
$action = $data[0];
|
|
$selected_items = $data[1];
|
|
|
|
if ($selected_items != false) {
|
|
switch($action) {
|
|
case '1':
|
|
// delete devices
|
|
foreach ($selected_items as $device_id) {
|
|
try {
|
|
$mc->table('cactiApplDeviceTable')->row($device_id)->delete();
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
|
|
try {
|
|
$mc->table('cactiStatsDeviceTable')->row($device_id)->delete();
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
}
|
|
|
|
// update total statistics
|
|
$mc->object('cactiStatsTotalsDevices')->set(snmpagent_read('cactiStatsTotalsDevices'));
|
|
$mc->object('cactiStatsTotalsDataSources')->set(snmpagent_read('cactiStatsTotalsDataSources'));
|
|
$mc->object('cactiStatsTotalsGraphs')->set(snmpagent_read('cactiStatsTotalsGraphs'));
|
|
$mc->object('cactiStatsLastUpdate')->set(time());
|
|
|
|
break;
|
|
case '2':
|
|
// enable devices
|
|
foreach ($selected_items as $device_id) {
|
|
$device_status = db_fetch_cell_prepared('SELECT status FROM host WHERE id = ?', [$device_id]);
|
|
|
|
try {
|
|
$mc->table('cactiApplDeviceTable')->row($device_id)->update(['cactiApplDeviceStatus' => $device_status]);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
}
|
|
$mc->object('cactiApplLastUpdate')->set(time());
|
|
|
|
break;
|
|
case '3':
|
|
// disable devices
|
|
foreach ($selected_items as $device_id) {
|
|
$device_status = db_fetch_cell_prepared('SELECT status FROM host WHERE id = ?', [$device_id]);
|
|
|
|
try {
|
|
$mc->table('cactiApplDeviceTable')->row($device_id)->update(['cactiApplDeviceStatus' => 4]);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
}
|
|
$mc->object('cactiApplLastUpdate')->set(time());
|
|
|
|
break;
|
|
case '5':
|
|
// clear device statistics
|
|
$values = [
|
|
'cactiStatsDeviceMinTime' => '9.99999',
|
|
'cactiStatsDeviceMaxTime' => '0',
|
|
'cactiStatsdeviceCurTime' => '0',
|
|
'cactiStatsDeviceAvgTime' => '0',
|
|
'cactiStatsDeviceTotalPolls' => '0',
|
|
'cactiStatsDeviceFailedPolls' => '0',
|
|
'cactiStatsDeviceAvailability' => '100'
|
|
];
|
|
|
|
foreach ($selected_items as $device_id) {
|
|
try {
|
|
$mc->table('cactiStatsDeviceTable')->row($device_id)->update($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
}
|
|
$mc->object('cactiStatsLastUpdate')->set(time());
|
|
|
|
break;
|
|
default:
|
|
// nothing to do
|
|
} // switch
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function snmpagent_poller_exiting(int $poller_index = 1) : bool {
|
|
if (!snmpagent_enabled()) {
|
|
return false;
|
|
}
|
|
|
|
$mc = new MibCache();
|
|
|
|
try {
|
|
$poller = $mc->table('cactiApplPollerTable')->row($poller_index)->select();
|
|
|
|
if ($poller == false) {
|
|
throw new Exception('Unable to find a poller');
|
|
}
|
|
|
|
$varbinds = [
|
|
'cactiApplPollerIndex' => $poller_index,
|
|
'cactiApplPollerHostname' => $poller['cactiApplPollerHostname'],
|
|
'cactiApplPollerIpAddress' => $poller['cactiApplPollerIpAddress']
|
|
];
|
|
|
|
snmpagent_notification('cactiNotifyPollerRuntimeExceeding', 'CACTI-MIB', $varbinds, SNMPAGENT_EVENT_SEVERITY_HIGH);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function snmpagent_poller_bottom() : bool {
|
|
if (!db_table_exists('snmpagent_cache')) {
|
|
return true;
|
|
}
|
|
|
|
if (!snmpagent_enabled()) {
|
|
snmpagent_cache_uninstall();
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!snmpagent_cache_initialized()) {
|
|
snmpagent_cache_rebuilt();
|
|
}
|
|
|
|
$device_in_maintenance = false;
|
|
|
|
$mc = new MibCache();
|
|
|
|
// START: update total device stats table
|
|
// deprecated
|
|
$devicestatus_indices = [0 => 0, 1 => 1, 2 => 2, 3 => 3, 4 => 4];
|
|
$current_states = db_fetch_assoc('SELECT status, COUNT(*) as cnt FROM `host` GROUP BY status');
|
|
|
|
if ($current_states && cacti_sizeof($current_states) > 0) {
|
|
foreach ($current_states as $current_state) {
|
|
$index = $devicestatus_indices[$current_state['status']];
|
|
$values = [
|
|
'cactiStatsTotalsDeviceStatusIndex' => $current_state['status'],
|
|
'cactiStatsTotalsDeviceStatusCounter' => $current_state['cnt']
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiStatsTotalsDeviceStatusTable')->row($index)->replace($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
unset($devicestatus_indices[$current_state['status']]);
|
|
}
|
|
}
|
|
|
|
if (cacti_sizeof($devicestatus_indices) > 0) {
|
|
foreach ($devicestatus_indices as $status => $index) {
|
|
$values = [
|
|
'cactiStatsTotalsDeviceStatusIndex' => $status,
|
|
'cactiStatsTotalsDeviceStatusCounter' => 0
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiStatsTotalsDeviceStatusTable')->row($index)->replace($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
$mc->object('cactiStatsTotalsDeviceStatusUnknown')->set(snmpagent_read('cactiStatsTotalsDeviceStatusUnknown'));
|
|
$mc->object('cactiStatsTotalsDeviceStatusDown')->set(snmpagent_read('cactiStatsTotalsDeviceStatusDown'));
|
|
$mc->object('cactiStatsTotalsDeviceStatusRecovering')->set(snmpagent_read('cactiStatsTotalsDeviceStatusRecovering'));
|
|
$mc->object('cactiStatsTotalsDeviceStatusUp')->set(snmpagent_read('cactiStatsTotalsDeviceStatusUp'));
|
|
$mc->object('cactiStatsTotalsDeviceStatusDisabled')->set(snmpagent_read('cactiStatsTotalsDeviceStatusDisabled'));
|
|
// END: update total device stats table
|
|
|
|
// update state and statistics of all devices
|
|
$mc_dstatus = [];
|
|
|
|
try {
|
|
$mc_devices = $mc->table('cactiApplDeviceTable')->select(['cactiApplDeviceIndex', 'cactiApplDeviceStatus']);
|
|
|
|
if ($mc_devices && cacti_sizeof($mc_devices)) {
|
|
foreach ($mc_devices as $mc_device) {
|
|
if (isset($mc_device['cactiApplDeviceStatus']) && isset($mc_device['cactiApplDeviceIndex'])) {
|
|
$mc_dstatus[$mc_device['cactiApplDeviceIndex']] = $mc_device['cactiApplDeviceStatus'];
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
$mc_dfailed = [];
|
|
|
|
try {
|
|
$mc_device_stats = $mc->table('cactiStatsDeviceTable')->select(['cactiStatsDeviceIndex', 'cactiStatsDeviceFailedPolls']);
|
|
|
|
if ($mc_device_stats && cacti_sizeof($mc_device_stats) > 0) {
|
|
foreach ($mc_device_stats as $mc_device_stat) {
|
|
if (isset($mc_device_stat['cactiStatsDeviceFailedPolls']) && isset($mc_device_stat['cactiStatsDeviceIndex'])) {
|
|
$mc_dfailed[$mc_device_stat['cactiStatsDeviceIndex']] = $mc_device_stat['cactiStatsDeviceFailedPolls'];
|
|
}
|
|
}
|
|
}
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
|
|
$devices = db_fetch_assoc('SELECT id, description, hostname, status,
|
|
disabled, status_event_count, status_fail_date, status_rec_date,
|
|
status_last_error, min_time, max_time, cur_time, avg_time,
|
|
total_polls, failed_polls, availability, snmp_engine_id
|
|
FROM host
|
|
ORDER BY id ASC');
|
|
|
|
if (cacti_sizeof($devices)) {
|
|
foreach ($devices as $device) {
|
|
$device_in_maintenance = api_plugin_hook_function('is_device_in_maintenance', $device['id']);
|
|
|
|
if (!$device_in_maintenance) {
|
|
$varbinds = [
|
|
'cactiApplDeviceIndex' => $device['id'],
|
|
'cactiApplDeviceDescription' => $device['description'],
|
|
'cactiApplDeviceHostname' => $device['hostname'],
|
|
'cactiApplDeviceLastError' => $device['status_last_error']
|
|
];
|
|
|
|
$overwrite['snmp_engine_id'] = $device['snmp_engine_id'];
|
|
|
|
if (isset($mc_dfailed[$device['id']]) && $device['failed_polls'] > $mc_dfailed[$device['id']]) {
|
|
snmpagent_notification('cactiNotifyDeviceFailedPoll', 'CACTI-MIB', $varbinds, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
|
|
}
|
|
|
|
if (isset($mc_dstatus[$device['id']]) && $mc_dstatus[$device['id']] == HOST_UP && $device['status'] == HOST_DOWN) {
|
|
snmpagent_notification('cactiNotifyDeviceDown', 'CACTI-MIB', $varbinds, SNMPAGENT_EVENT_SEVERITY_HIGH, $overwrite);
|
|
} elseif (isset($mc_dstatus[$device['id']]) && $mc_dstatus[$device['id']] == HOST_DOWN && $device['status'] == HOST_RECOVERING) {
|
|
snmpagent_notification('cactiNotifyDeviceRecovering', 'CACTI-MIB', $varbinds, SNMPAGENT_EVENT_SEVERITY_MEDIUM, $overwrite);
|
|
}
|
|
}
|
|
|
|
$values = [
|
|
'cactiApplDeviceStatus' => ($device['disabled'] == 'on') ? 4 : $device['status'],
|
|
'cactiApplDeviceEventCount' => $device['status_event_count'],
|
|
'cactiApplDeviceFailDate' => $device['status_fail_date'],
|
|
'cactiApplDeviceRecoveryDate' => $device['status_rec_date'],
|
|
'cactiApplDeviceLastError' => $device['status_last_error']
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiApplDeviceTable')->row($device['id'])->update($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
|
|
$values = [
|
|
'cactiStatsDeviceMinTime' => $device['min_time'],
|
|
'cactiStatsDeviceMaxTime' => $device['max_time'],
|
|
'cactiStatsDeviceCurTime' => $device['cur_time'],
|
|
'cactiStatsDeviceAvgTime' => $device['avg_time'],
|
|
'cactiStatsDeviceTotalPolls' => $device['total_polls'],
|
|
'cactiStatsDeviceFailedPolls' => $device['failed_polls'],
|
|
'cactiStatsDeviceAvailability' => $device['availability']
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiStatsDeviceTable')->row($device['id'])->update($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// get a list of all plugins available on that system
|
|
$pluginslist = snmpagent_get_pluginslist();
|
|
|
|
// truncate plugin mib table
|
|
try {
|
|
$mc->table('cactiApplPluginTable')->truncate();
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
|
|
// refill plugin mib table
|
|
if ($pluginslist && cacti_sizeof($pluginslist) > 0) {
|
|
$i = 1;
|
|
|
|
foreach ($pluginslist as $plugin) {
|
|
$values = [
|
|
'cactiApplPluginIndex' => $i,
|
|
'cactiApplPluginType' => 2,
|
|
'cactiApplPluginName' => $plugin['directory'],
|
|
'cactiApplPluginStatus' => $plugin['status'],
|
|
'cactiApplPluginVersion' => $plugin['version']
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiApplPluginTable')->row($i)->insert($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
$i++;
|
|
}
|
|
}
|
|
$mc->object('cactiApplLastUpdate')->set(time());
|
|
|
|
$recache_stats = db_fetch_cell("SELECT value
|
|
FROM settings
|
|
WHERE name = 'stats_recache'");
|
|
|
|
if ($recache_stats) {
|
|
[$time, $hosts] = explode(' ', $recache_stats);
|
|
$time = str_replace('RecacheTime:', '', $time);
|
|
$hosts = str_replace('HostsRecached:', '', $hosts);
|
|
|
|
$mc->object('cactiStatsRecacheTime')->set($time);
|
|
$mc->object('cactiStatsRecachedHosts')->set($hosts);
|
|
}
|
|
$mc->object('cactiStatsLastUpdate')->set(time());
|
|
|
|
// clean up the notification log
|
|
$snmp_notification_managers = db_fetch_assoc('SELECT id, max_log_size FROM snmpagent_managers');
|
|
|
|
if ($snmp_notification_managers && cacti_sizeof($snmp_notification_managers) > 0) {
|
|
foreach ($snmp_notification_managers as $snmp_notification_manager) {
|
|
db_execute_prepared('DELETE FROM snmpagent_notifications_log
|
|
WHERE manager_id = ?
|
|
AND `time` <= ?',
|
|
[$snmp_notification_manager['id'], time() - 86400 * $snmp_notification_manager['max_log_size']]);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function snmpagent_get_pluginslist() : array {
|
|
global $plugins, $plugins_integrated;
|
|
/* update the list of known plugins only once per polling cycle. In all other cases we would
|
|
have to create too many new hooks to update that MIB table just in time.
|
|
We have to do the same like function plugins_load_temp_table(), which will not be available
|
|
during the execution of that function. */
|
|
|
|
$pluginslist = [];
|
|
$registered_plugins = db_fetch_assoc('SELECT * FROM plugin_config ORDER BY name');
|
|
|
|
foreach ($registered_plugins as $t) {
|
|
$pluginslist[$t['directory']] = $t;
|
|
}
|
|
|
|
$path = CACTI_PATH_PLUGINS . '/';
|
|
$dh = opendir($path);
|
|
|
|
if ($dh !== false) {
|
|
while (($file = readdir($dh)) !== false) {
|
|
if ((is_dir("$path$file")) && !in_array($file, $plugins_integrated, true) &&
|
|
(file_exists("$path$file/setup.php")) && (!array_key_exists($file, $pluginslist))) {
|
|
if (file_exists("$path$file/INFO")) {
|
|
$cinfo = plugin_load_info_file("$path$file/INFO");
|
|
$pluginslist[$file] = $cinfo;
|
|
}
|
|
}
|
|
}
|
|
|
|
closedir($dh);
|
|
}
|
|
|
|
return $pluginslist;
|
|
}
|
|
|
|
/**
|
|
* snmpagent_cache_install()
|
|
* Generates a SNMP caching tables reflecting all objects of the Cacti MIB
|
|
*
|
|
* @return bool
|
|
*/
|
|
function snmpagent_cache_install() : bool {
|
|
if (!snmpagent_enabled()) {
|
|
return false;
|
|
}
|
|
|
|
// drop everything
|
|
snmpagent_cache_uninstall();
|
|
|
|
$mc = new MibCache();
|
|
$mc->install(CACTI_PATH_MIBS . '/CACTI-MIB');
|
|
$mc->install(CACTI_PATH_MIBS . '/CACTI-SNMPAGENT-MIB');
|
|
$mc->install(CACTI_PATH_MIBS . '/CACTI-BOOST-MIB');
|
|
snmpagent_cache_init();
|
|
|
|
// call install routine of plugins supporting the SNMPAgent
|
|
api_plugin_hook('snmpagent_cache_install');
|
|
|
|
return true;
|
|
}
|
|
|
|
function snmpagent_cache_uninstall() : void {
|
|
// drop everything if not empty
|
|
|
|
$tables = [
|
|
'snmpagent_cache',
|
|
'snmpagent_mibs',
|
|
'snmpagent_cache_notifications',
|
|
'snmpagent_cache_textual_conventions'
|
|
];
|
|
|
|
foreach ($tables as $table) {
|
|
$rows = db_fetch_cell("SELECT COUNT(*) FROM $table");
|
|
|
|
if ($rows > 0) {
|
|
db_execute("TRUNCATE $table");
|
|
}
|
|
}
|
|
}
|
|
|
|
function snmpagent_cache_initialized() : mixed {
|
|
return db_fetch_cell('SELECT COUNT(*) FROM `snmpagent_cache`') > 0;
|
|
}
|
|
|
|
function snmpagent_cache_rebuilt() : void {
|
|
snmpagent_cache_install();
|
|
}
|
|
|
|
function snmpagent_cache_init() : void {
|
|
/* fill up the cache with a minimum of data and ignore all values that
|
|
* will be updated automatically at the bottom of the next poller run
|
|
*/
|
|
$mc = new MibCache();
|
|
|
|
// update global settings
|
|
snmpagent_global_settings_update();
|
|
|
|
// add pollers of a distributed system (future)
|
|
$pollers = db_fetch_assoc('SELECT id FROM poller ORDER BY id ASC');
|
|
|
|
if ($pollers && cacti_sizeof($pollers) > 0) {
|
|
foreach ($pollers as $poller) {
|
|
$poller_data = db_fetch_row_prepared('SELECT * FROM poller WHERE id = ?', [$poller['id']]);
|
|
}
|
|
} else {
|
|
// this is NOT a distributed system, but it should have at least one local poller.
|
|
$poller_lastrun = read_config_option('poller_lastrun', true);
|
|
$values = [
|
|
'cactiApplPollerIndex' => 1,
|
|
'cactiApplPollerHostname' => 'localhost',
|
|
'cactiApplPollerIpAddress' => '127.0.0.1',
|
|
'cactiApplPollerLastUpdate' => $poller_lastrun
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiApplPollerTable')->row(1)->insert($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
|
|
$values = [
|
|
'cactiStatsPollerIndex' => 1,
|
|
'cactiStatsPollerHostname' => 'localhost',
|
|
'cactiStatsPollerMethod' => read_config_option('poller_type', true)
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiStatsPollerTable')->row(1)->insert($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
}
|
|
|
|
// add all devices as devicetable entries to the snmp cache
|
|
$devices = db_fetch_assoc('SELECT id, description, hostname, disabled, status_event_count, status_fail_date,
|
|
status_rec_date, status_last_error, min_time, max_time, cur_time,
|
|
avg_time, total_polls, failed_polls, availability
|
|
FROM host
|
|
ORDER BY id ASC');
|
|
|
|
if (cacti_sizeof($devices)) {
|
|
foreach ($devices as $device) {
|
|
$device = db_fetch_row_prepared('SELECT * FROM host WHERE id = ?', [$device['id']]);
|
|
|
|
// add device to cactiApplDeviceTable
|
|
if (cacti_sizeof($device)) {
|
|
$values = [
|
|
'cactiApplDeviceIndex' => $device['id'],
|
|
'cactiApplDeviceDescription' => $device['description'],
|
|
'cactiApplDeviceHostname' => $device['hostname'],
|
|
'cactiApplDeviceStatus' => ($device['disabled'] == 'on') ? 4 : $device['status'],
|
|
'cactiApplDeviceEventCount' => $device['status_event_count'],
|
|
'cactiApplDeviceFailDate' => $device['status_fail_date'],
|
|
'cactiApplDeviceRecoveryDate' => $device['status_rec_date'],
|
|
'cactiApplDeviceLastError' => $device['status_last_error'],
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiApplDeviceTable')->row($device['id'])->insert($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
|
|
// add device to cactiStatsDeviceTable
|
|
$values = [
|
|
'cactiStatsDeviceIndex' => $device['id'],
|
|
'cactiStatsDeviceHostname' => $device['hostname'],
|
|
'cactiStatsDeviceMinTime' => $device['min_time'],
|
|
'cactiStatsDeviceMaxTime' => $device['max_time'],
|
|
'cactiStatsDeviceCurTime' => $device['cur_time'],
|
|
'cactiStatsDeviceAvgTime' => $device['avg_time'],
|
|
'cactiStatsDeviceTotalPolls' => $device['total_polls'],
|
|
'cactiStatsDeviceFailedPolls' => $device['failed_polls'],
|
|
'cactiStatsDeviceAvailability' => $device['availability']
|
|
];
|
|
|
|
try {
|
|
$mc->table('cactiStatsDeviceTable')->row($device['id'])->insert($values);
|
|
} catch (Exception $e) {
|
|
cacti_log('WARNING: SNMPAgent: ' . $e->getMessage(), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function snmpagent_read(string $object) : mixed {
|
|
switch($object) {
|
|
case 'cactiApplVersion':
|
|
$value = db_fetch_cell('SELECT `cacti` FROM `version`');
|
|
|
|
break;
|
|
case 'cactiApplSnmpVersion':
|
|
$snmp_version = read_config_option('snmp_version', true);
|
|
$value = $snmp_version;
|
|
|
|
if (function_exists('snmpget')) {
|
|
$value = 3;
|
|
}
|
|
|
|
break;
|
|
case 'cactiStatsTotalsDevices':
|
|
$value = db_fetch_cell('SELECT COUNT(*) FROM host');
|
|
|
|
break;
|
|
case 'cactiStatsTotalsDataSources':
|
|
$value = db_fetch_cell('SELECT COUNT(*) FROM data_local');
|
|
|
|
break;
|
|
case 'cactiStatsTotalsGraphs':
|
|
$value = db_fetch_cell('SELECT COUNT(*) FROM graph_local');
|
|
|
|
break;
|
|
case 'cactiStatsTotalsDeviceStatusUnknown':
|
|
$value = db_fetch_cell('SELECT COUNT(*) FROM host WHERE status = 0');
|
|
|
|
break;
|
|
case 'cactiStatsTotalsDeviceStatusDown':
|
|
$value = db_fetch_cell('SELECT COUNT(*) FROM host WHERE status = 1');
|
|
|
|
break;
|
|
case 'cactiStatsTotalsDeviceStatusRecovering':
|
|
$value = db_fetch_cell('SELECT COUNT(*) FROM host WHERE status = 2');
|
|
|
|
break;
|
|
case 'cactiStatsTotalsDeviceStatusUp':
|
|
$value = db_fetch_cell('SELECT COUNT(*) FROM host WHERE status = 3');
|
|
|
|
break;
|
|
case 'cactiStatsTotalsDeviceStatusDisabled':
|
|
$value = db_fetch_cell('SELECT COUNT(*) FROM host WHERE status = 4');
|
|
|
|
break;
|
|
default:
|
|
$value = false;
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
function snmpagent_notification(string $notification, string $mib, array $varbinds, int $severity = SNMPAGENT_EVENT_SEVERITY_MEDIUM, mixed $overwrite = false) : bool {
|
|
global $config, $snmpagent_event_severity;
|
|
|
|
if (isset($config['snmpagent']['notifications']['ignore'][$notification])) {
|
|
return false;
|
|
}
|
|
|
|
$path_snmptrap = read_config_option('path_snmptrap');
|
|
|
|
if (!in_array($severity, [SNMPAGENT_EVENT_SEVERITY_LOW, SNMPAGENT_EVENT_SEVERITY_MEDIUM, SNMPAGENT_EVENT_SEVERITY_HIGH, SNMPAGENT_EVENT_SEVERITY_CRITICAL], true)) {
|
|
cacti_log('ERROR: Unknown event severity: "' . $severity . '" for ' . $notification . ' (' . $mib . ')', false, 'SNMPAGENT', POLLER_VERBOSITY_NONE);
|
|
|
|
return false;
|
|
}
|
|
|
|
$enterprise_oid = db_fetch_cell_prepared('SELECT oid
|
|
FROM snmpagent_cache
|
|
WHERE `name` = ?
|
|
AND `mib` = ?',
|
|
[$notification, $mib]);
|
|
|
|
if (!$enterprise_oid) {
|
|
// system does not know this event
|
|
cacti_log('ERROR: Unknown event: ' . $notification . ' (' . $mib . ')', false, 'SNMPAGENT', POLLER_VERBOSITY_NONE);
|
|
|
|
return false;
|
|
} else {
|
|
$branches = explode('.', $enterprise_oid);
|
|
$specific_trap_number = array_pop($branches);
|
|
}
|
|
|
|
// generate a list of SNMP notification receivers listening for this notification
|
|
$notification_managers = db_fetch_assoc_prepared('SELECT snmpagent_managers.*
|
|
FROM snmpagent_managers_notifications
|
|
INNER JOIN snmpagent_managers
|
|
ON (snmpagent_managers.id = snmpagent_managers_notifications.manager_id)
|
|
WHERE snmpagent_managers.disabled = 0
|
|
AND snmpagent_managers_notifications.notification = ?
|
|
AND snmpagent_managers_notifications.mib = ?',
|
|
[$notification, $mib]);
|
|
|
|
if (cacti_sizeof($notification_managers) == 0) {
|
|
// No receivers found for the message, record it to the cacti.log
|
|
cacti_log('NOTICE: No enabled SNMP notification receivers are configured for event: ' . $notification . ' (' . $mib . '), severity: ' . $snmpagent_event_severity[$severity] . '. Configure or enable receivers under Console > Utilities > SNMP Managers, or ignore this notice when SNMP traps are intentionally disabled.', false, 'SNMPAGENT', POLLER_VERBOSITY_NONE);
|
|
|
|
if (!in_array($severity, [SNMPAGENT_EVENT_SEVERITY_HIGH, SNMPAGENT_EVENT_SEVERITY_CRITICAL], true)) {
|
|
// Prevent log spam of messages lower than a high severity
|
|
$config['snmpagent']['notifications']['ignore'][$notification] = 1;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
$registered_var_binds = [];
|
|
|
|
// get a list of registered var binds
|
|
$reg_var_binds = db_fetch_assoc_prepared('SELECT
|
|
scn.attribute, sc.oid, sc.type, sctc.type as tcType
|
|
FROM snmpagent_cache_notifications AS scn
|
|
LEFT JOIN snmpagent_cache AS sc
|
|
ON sc.mib = scn.mib AND sc.name = scn.attribute
|
|
LEFT JOIN snmpagent_cache_textual_conventions AS sctc
|
|
ON sc.mib = sctc.mib AND sc.type = sctc.name
|
|
WHERE scn.name = ?
|
|
AND scn.mib = ?
|
|
ORDER BY scn.sequence_id',
|
|
[$notification, $mib]);
|
|
|
|
if (cacti_sizeof($reg_var_binds)) {
|
|
foreach ($reg_var_binds as $reg_var_bind) {
|
|
$registered_var_binds[$reg_var_bind['attribute']] = [
|
|
'oid' => $reg_var_bind['oid'],
|
|
'type' => $reg_var_bind['tcType'] ?: $reg_var_bind['type']
|
|
];
|
|
}
|
|
}
|
|
|
|
$difference = array_diff(array_keys($registered_var_binds), array_keys($varbinds));
|
|
|
|
if (cacti_sizeof($difference) == 0) {
|
|
/* order the managers by message type to send out all notifications immediately. Informs
|
|
will take more processing time.
|
|
*/
|
|
$notification_managers = db_fetch_assoc_prepared('SELECT sm.*
|
|
FROM snmpagent_managers_notifications AS smn
|
|
INNER JOIN snmpagent_managers AS sm
|
|
ON sm.id = smn.manager_id
|
|
WHERE smn.notification = ?
|
|
AND smn.mib = ?
|
|
ORDER BY sm.snmp_message_type', [$notification, $mib]);
|
|
|
|
if (cacti_sizeof($notification_managers)) {
|
|
include_once(CACTI_PATH_LIBRARY . '/poller.php');
|
|
|
|
/*
|
|
TYPE: one of i, u, t, a, o, s, x, d, b
|
|
i: INTEGER, u: unsigned INTEGER, t: TIMETICKS, a: IPADDRESS
|
|
o: OBJID, s: STRING, x: HEX STRING, d: DECIMAL STRING, b: BITS
|
|
U: unsigned int64, I: signed int64, F: float, D: double
|
|
*/
|
|
$smi2netsnmp_datatypes = [
|
|
'integer' => 'i',
|
|
'integer32' => 'i',
|
|
'unsigned32' => 'u',
|
|
'gauge' => 'i',
|
|
'gauge32' => 'i',
|
|
'counter' => 'i',
|
|
'counter32' => 'i',
|
|
'counter64' => 'I',
|
|
'timeticks' => 't',
|
|
'octet string' => 's',
|
|
'opaque' => 's',
|
|
'object identifier' => 'o',
|
|
'ipaddress' => 'a',
|
|
'networkaddress' => 'IpAddress',
|
|
'bits' => 'b',
|
|
'displaystring' => 's',
|
|
'physaddress' => 's',
|
|
'macaddress' => 's',
|
|
'truthvalue' => 'i',
|
|
'testandincr' => 'i',
|
|
'autonomoustype' => 'o',
|
|
'variablepointer' => 'o',
|
|
'rowpointer' => 'o',
|
|
'rowstatus' => 'i',
|
|
'timestamp' => 't',
|
|
'timeinterval' => 'i',
|
|
'dateandtime' => 's',
|
|
'storagetype' => 'i',
|
|
'tdomain' => 'o',
|
|
'taddress' => 's'
|
|
];
|
|
|
|
$log_notification_varbinds = '';
|
|
|
|
// Each token is escaped individually below; building the varbind triples
|
|
// as discrete array elements keeps values with spaces or shell metacharacters
|
|
// from splitting or altering the command line.
|
|
$snmp_notification_varbinds = [];
|
|
|
|
$args = [];
|
|
|
|
foreach ($notification_managers as $notification_manager) {
|
|
// the receiver hostname is attacker-controlled device data; strip '%'
|
|
// here so cmd.exe cannot expand an environment variable from it.
|
|
$snmp_trap_receiver = (CACTI_SERVER_OS == 'win32' ? str_replace('%', '', $notification_manager['hostname']) : $notification_manager['hostname']) . ':' . $notification_manager['snmp_port'];
|
|
|
|
if (!cacti_sizeof($snmp_notification_varbinds)) {
|
|
foreach ($registered_var_binds as $name => $attributes) {
|
|
$snmp_notification_varbinds[] = $attributes['oid'];
|
|
$snmp_notification_varbinds[] = $smi2netsnmp_datatypes[cacti_strtolower($attributes['type'])];
|
|
$snmp_notification_varbinds[] = str_replace('"', "'", $varbinds[$name]);
|
|
$log_notification_varbinds .= $name . ':"' . str_replace('"', "'", $varbinds[$name]) . '" ';
|
|
}
|
|
}
|
|
|
|
if ($notification_manager['snmp_version'] == 1) {
|
|
$args = array_merge([
|
|
'-v', '1',
|
|
'-c', $notification_manager['snmp_community'],
|
|
$snmp_trap_receiver,
|
|
$enterprise_oid,
|
|
'', '6', $specific_trap_number, ''
|
|
], $snmp_notification_varbinds);
|
|
} elseif ($notification_manager['snmp_version'] == 2) {
|
|
$args = ['-v', '2c', '-c', $notification_manager['snmp_community']];
|
|
|
|
if ($notification_manager['snmp_message_type'] == 2) {
|
|
$args[] = '-Ci';
|
|
}
|
|
|
|
$args = array_merge($args, [
|
|
$snmp_trap_receiver,
|
|
'', $enterprise_oid
|
|
], $snmp_notification_varbinds);
|
|
} elseif ($notification_manager['snmp_version'] == 3) {
|
|
if ($overwrite && isset($overwrite['snmp_engine_id']) && $overwrite['snmp_engine_id']) {
|
|
$notification_manager['snmp_engine_id'] = $overwrite['snmp_engine_id'];
|
|
}
|
|
|
|
$args = ['-v', '3', '-e', $notification_manager['snmp_engine_id']];
|
|
|
|
if ($notification_manager['snmp_message_type'] == 2) {
|
|
$args[] = '-Ci';
|
|
}
|
|
|
|
$args[] = '-u';
|
|
$args[] = $notification_manager['snmp_username'];
|
|
|
|
if ($notification_manager['snmp_password'] && $notification_manager['snmp_priv_passphrase']) {
|
|
$snmp_security_level = 'authPriv';
|
|
} elseif ($notification_manager['snmp_password']) {
|
|
$snmp_security_level = 'authNoPriv';
|
|
} else {
|
|
$snmp_security_level = 'noAuthNoPriv';
|
|
}
|
|
|
|
$args[] = '-l';
|
|
$args[] = $snmp_security_level;
|
|
|
|
if ($snmp_security_level != 'noAuthNoPriv') {
|
|
$args[] = '-a';
|
|
$args[] = $notification_manager['snmp_auth_protocol'];
|
|
$args[] = '-A';
|
|
$args[] = $notification_manager['snmp_password'];
|
|
}
|
|
|
|
if ($snmp_security_level == 'authPriv') {
|
|
$args[] = '-x';
|
|
$args[] = $notification_manager['snmp_priv_protocol'];
|
|
$args[] = '-X';
|
|
$args[] = $notification_manager['snmp_priv_passphrase'];
|
|
}
|
|
|
|
$args = array_merge($args, [
|
|
$snmp_trap_receiver,
|
|
'', $enterprise_oid
|
|
], $snmp_notification_varbinds);
|
|
}
|
|
|
|
// execute net-snmp to generate this notification in the background.
|
|
// snmptrap relies on fixed positional arguments (agent address and
|
|
// uptime placeholders) that are passed empty here; escape each token
|
|
// directly so empty positions survive as quoted '' rather than being
|
|
// swallowed when exec_background joins the arguments with spaces.
|
|
$escaped_args = implode(' ', array_map(static function ($arg) {
|
|
return $arg === '' ? "''" : cacti_escapeshellarg_cmd($arg);
|
|
}, $args));
|
|
|
|
exec_background(cacti_escapeshellcmd($path_snmptrap), $escaped_args);
|
|
|
|
// insert a new entry into the notification log for that SNMP receiver
|
|
$save = [];
|
|
$save['id'] = 0;
|
|
$save['time'] = time();
|
|
$save['severity'] = $severity;
|
|
$save['manager_id'] = $notification_manager['id'];
|
|
$save['notification'] = $notification;
|
|
$save['mib'] = $mib;
|
|
$save['varbinds'] = substr($log_notification_varbinds, 0, 5000);
|
|
|
|
sql_save($save, 'snmpagent_notifications_log');
|
|
|
|
// log the net-snmp command for Cacti admins if they wish for
|
|
cacti_log("NOTE: $path_snmptrap " . str_replace([$notification_manager['snmp_password'], $notification_manager['snmp_priv_passphrase']], '********', implode(' ', $args)), false, 'SNMPAGENT', POLLER_VERBOSITY_MEDIUM);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
} else {
|
|
// mismatching number of var binds
|
|
cacti_log('ERROR: Incomplete number of varbinds given for event: ' . $notification . ' (' . $mib . ')', false, 'SNMPAGENT', POLLER_VERBOSITY_NONE);
|
|
|
|
return false;
|
|
}
|
|
}
|