* PHPStan Level 8 - Phase 1 Conversion
Reduces the real (no-baseline) PHPStan level 8 error count from ~2,724 to 425
and regenerates phpstan-baseline.neon accordingly (8,941 -> 1,921 lines).
Core type-safety changes
- lib/database.php: narrow db_fetch_row()/db_fetch_row_prepared() to return
array (was bool|array) and db_fetch_assoc()/db_fetch_assoc_prepared() to
return array (was mixed); the _prepared workers coerce the connection-failure
path to an empty array so the declared type is honest. empty()/cacti_sizeof()
checks are unaffected.
- lib/database.php: narrow $db_conn guards from is_object() to instanceof PDO in
the transaction/quote/insert-id helpers so the PDO calls resolve.
- lib/mib_cache.php: the fluent MibCache methods mib()/object()/table()/row()
now return self instead of a bare object so chained calls type-check.
Type-safety casts and guards (mechanical, behaviour-preserving)
- Cast string|false/|null builtin results feeding string parameters
(json_decode, base64_encode, trim, htmle, preg_replace, explode, etc.).
- Cast int|string / int|false values feeding int id/pid parameters.
- Guard resource|false fopen()/popen() handles before fwrite/fclose/fread, and
wrap foreach over array|false/list|false results with `?: []`.
Genuine bugs found and fixed
- cli/add_tree.php / lib/api_tree.php: site tree nodes could not be created via
the CLI - the --site-id option was never parsed, $nodeTypes had no 'site'
entry (passing null to api_tree_item_save(int $type) -> TypeError), and
api_tree_item_save() never persisted site_id. All three fixed.
- tree.php: leaves_exist() is typed : int but returned db_fetch_assoc_prepared()
(an array of rows) for a SELECT COUNT(*); now uses db_fetch_cell_prepared()
and returns the scalar count.
- lib/mib_cache.php: $entries_per_object was a float (division / ceil) used as
an array key (PHP 8.1 implicit float-to-int key deprecation); now cast to int.
Config
- .phpstan.neon: add notIdentical.alwaysTrue and booleanAnd.alwaysFalse to the
existing always-true/false ignore family (same "PHPStan gets confused"
dynamic-constant cause already covered by identical.alwaysTrue,
notEqual.alwaysTrue, booleanAnd.leftAlwaysTrue, etc.).
- phpstan-baseline.neon: regenerated; the remaining 425 entries are the
untouched long tail for Phase 2.
* Remove redundant @phpstan-ignore comments
Follow-up cleanup for the level 8 Phase 1 work. Removes 14 inline
@phpstan-ignore comments that no longer suppress any reported error:
- cli/repair_database.php: `@phpstan-ignore booleanAnd.alwaysFalse` is now
covered by the global booleanAnd.alwaysFalse ignore added to .phpstan.neon.
- 13 others (host.php, include/global.php, include/session.php,
lib/api_automation.php x2, lib/functions.php x2, lib/html_utility.php x2,
lib/installer.php, lib/package.php, lib/reports.php, lib/rrd.php) no longer
match any reported error after the type-safety fixes (e.g. the
db_fetch_row_prepared() return-type narrowing removed the offset-assign
errors the api_automation ignores were suppressing).
Verified with reportUnmatchedIgnoredErrors enabled: 0 unmatched ignores
remain and the full level 8 run still reports 0 errors.
* ci: satisfy validator ratchet and refresh security sink baselines
- lib/api_tree.php: give the new tree-item site_id a real numeric
constraint ('^[-0-9]+$', matching html_reports.php) instead of an
empty regex, so the validator constraint ratchet passes without
bumping its baseline.
- tests/security/baselines: refresh the three sink/hotspot signatures
whose code text changed from the (string) casts added in this branch
(lib/poller.php setsid, cli/rrdresize.php json_encode, lib/import.php
simplexml_load_string).
* ci(js): allow DOMPurify 3.4.16 and update sync_js pin test
The dependabot bump (#8134) moved package.json/package-lock.json to
DOMPurify 3.4.16 on develop but left tests/js/sync_js.test.mjs pinned to
3.4.15, so the PR merge commit fails the 'DOMPurify is pinned' contract.
Bring package.json/package-lock.json to 3.4.16 (matching develop) and
update the pin test to expect 3.4.16.
* test: update sql.php null-handling assertion for the (string) cast
This branch changed scripts/sql.php from trim(\ ?? '') to
trim((string) \) (the phpstan-safe form that coerces both null and
false from shell_exec()). Update SqlScriptsTest's null-return assertion
to match the new cast instead of the removed '?? ''.
* fix: address Copilot review - ownership IDs, fopen guards, mib_cache bound
- chown()/chgrp(): pass integer UID/GID from fileowner()/filegroup()
(guarding the false lookup) instead of string-casting them, which made
them resolve account/group names like '1001' and fail. Covers
lib/api_device.php, lib/boost.php and poller_maintenance.php (x2).
- fopen() failure now short-circuits instead of reporting success:
lib/package.php returns false, lib/clog_webapi.php and utilities.php
render an error, and snmpagent_mibcachechild.php skips the cache write
and sentinel unlink so a concurrent writer's lock is preserved.
- lib/mib_cache.php: keep the float quotient as the loop bound and cast
only the array offset, so a non-even entries/columns split no longer
truncates the loop and drops a cached row.
* revert: keep db_fetch_* returning false on error; baseline Level 8 instead
Per review, restoring the intentional fail-safe: db_fetch_row[_prepared]()
return bool|array and db_fetch_assoc[_prepared]() return mixed again, so a
genuine SQL error (not just a missing connection) is signalled as false and
the === false checks in cdef_is_in_use(), boost_process_* and aggregate.php
work as before. Dropped the is_array() ? : [] coercion.
Level 8 stays green by regenerating phpstan-baseline.neon to capture the
re-introduced bool|array/mixed call-site errors (the ~1,850 the narrowing
had eliminated). Tightening those callers to drop them from the baseline is
Phase 2 work.
---------
Co-authored-by: TheWitness <copilot@users.noreply.github.com>
* 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
* 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>
* fix: detect a failed SNMP walk by exit code, not by device data
cacti_snmp_walk() discarded the entire walk when its output contained the word
Timeout. That output is the walk itself, one line per OID, so a device with an
ifAlias of 'Timeout Monitor' or a matching sysDescr emptied the result and
data_query.php then dropped every index for the device.
The check could not detect a real timeout either. net-snmp writes that to
stderr and neither walk command redirects it, so the matched string was always
device data. cacti_snmp_get() already reads the exit code for exactly this
reason; exec_into_array() exposes it by reference.
Also refuse a walk with empty credentials, matching cacti_snmp_get, get_raw and
getnext. An unknown v3 protocol previously built a credential-less snmpwalk and
failed with no log line.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: cover the credential guard and refresh the security baselines
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: give the exec_into_array stub the real signature
The stub dropped the by-reference exit code that lib/functions.php exposes, so
any code reading it saw 0 regardless of what the command returned. That is why
the walk's failure detection could regress to matching stdout without a test
noticing.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: expect the exit-code wording for a failed walk
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
cacti_snmp_session() takes fifteen positional arguments. The poller, data
queries, and automation each spelled out the same $host-row mapping, which is
where SNMPv3 credential handling drifted between callers. This adds one wrapper
that assembles the arguments, and converts the two data_query.php call sites as
the first adopters. Missing keys fall back to cacti_snmp_session()'s own
defaults, so behavior is unchanged.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Force the SNMP port to an integer in cacti_snmp_options_sanitize
Every net-snmp exec path interpolates $port raw into the command line
(hostname:port). The sanitizer only defaulted an empty port to 161 without
casting, so a non-numeric value passed through verbatim. Cast it to an integer
(falling back to 161 for empty/non-numeric input) so it can never carry shell
metacharacters, and cast the port in the ext-based SNMP session target too.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Move the 1.3.0-dev changelog entry to the end of the issue block
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
* fix: harden CSV encoding and process execution handling
Route CSV export cells through a shared cacti_csv_cell() helper so values with quotes, commas, or leading formula characters round-trip correctly, and add defensive guards on shell construction, unchecked proc_open handles, discarded exec exit codes, and gap values in nth-percentile fetch math.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix: address review feedback on CSV and process hardening
- escape poller_id in cmd_realtime proc_open command to match remote_agent
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix: keep numeric CSV cells unquoted and guard snmptrap and rrdtool process failures
- exempt numeric values from the CSV formula-injection prefix and preserve empty snmptrap positions while bailing out when the RRDtool process fails to spawn
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* ci: ignore phpstan function.alreadyNarrowedType identifier
Clears the level 6 alreadyNarrowedType error on lib/data_query.php:2591 that
develop currently emits, matching the sibling narrowing identifiers already ignored.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
* cleanup: remove unreachable defensive error logs in aggregate graph item validation
* fix: replace strtolower/strtoupper with locale-independent wrappers
PHP 8.5 deprecates locale-sensitive strtolower() and strtoupper().
Add cacti_strtolower() and cacti_strtoupper() wrappers that use
mb_strtolower/mb_strtoupper with explicit UTF-8 encoding, then
replace all 185 call sites across 65 files.
Existing mb_strtolower() calls (email normalization) are left as-is
since they already specify encoding explicitly.
Includes 18 Pest tests covering ASCII, UTF-8, protocol names, config
values, hex strings, NaN detection, and file extension patterns.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix: correct docblock references and month_shift() strpos check
Fix docblocks to reference strtolower()/strtoupper() (the deprecated
PHP builtins) instead of the wrapper names. Fix month_shift() to use
strpos() !== false instead of > 0 so 'month' at position 0 is matched.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* chore: drop unrelated lib/api_aggregate changes
Keep PR scope focused on the issue for this branch.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
* fix: replace deprecated (double) cast with (float)
PHP 8.4 deprecates the non-canonical (double) cast. Replace all 11
instances with the canonical (float) equivalent. No behavioral change.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix: use float literals instead of no-op left-side casts
Left-side casts like (float) $var = 0 are no-ops — the cast is
discarded before assignment. Use 0.0 float literals instead.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
* Allow form_selectable_cell to more easily port
* Removing exclusion for scripts directory
* More fixes for scripts
* Some issues with ss_host_disk.php that passed locally
* More issues not detected locally
* Issues with typing in cacti_snmp_walk
* Adding base files for non-savy composer users
* One more try on ss_host_disk.php
* Aparantly I have to add the lock file
* More changes to lib/snmp.php after testing
* More changes to lib/snmp.php after testing
* Fix number script from Copilot review
* More Composer reviews
* Update scripts/ss_apache_stats.php
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update scripts/motorola_surfboard_SB6141.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update scripts/motorola_surfboard_SB6141.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Changing syntax of PHP requirement
* Updating composer
* Attempt to quiet some warnings
* Add Syntax Check
* Wrong file name
* Wrong file name
* Better to report before and after
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Fix#6539 - Allow Standard Cacti Log Files to be Rotated on Demand from the Cacti UI
* QA: php-cs-fixer updates and documentation for lib/database.php
* QA: Update .gitignore for composer files
* QA: Update .gitignore for composer files
* QA: Back off .gitignore change its duplicated
* QA: Fix typing of db_has_permissions
* QA: Fixing typing issue in db_format_index_create
* Update lib/clog_webapi.php
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* QA: Fix additional code issues
* QA: Applying more defaults to add device
* QA: Fix at the database layer
* QA: Fixes based upon automated testing
* QA: Fix at the database layer
* QA: After new SQL checking
* QA: Skip MacTrack oui database download warnings
* QA: Fixing type issue
* QA: Recommendation from Copilot
* QA: Recommendation from Copilot
* QA: Recommendation from Copilot
* QA: Recommendation from Copilot
* QA: Some minor changes to avoid errors in testing
* QA: Unused variable
* QA: Recommendation from Copilot
* QA: Recommendation from Copilot
* QA: Recommendation from Copilot
* QA: Minor introduced code defect
* QA: Adding some unifying space around concatination
* QA: Consistent comments and new function alias'
* QA: Multi-line comment corrections
* QA: Fix SQL Style
* QA: Missed one function alias
* QA: PHPStan Level 2 and PHPDoc fixes
* QA: PHPStan Level 2 and PHPDoc Fixes
* QA: PHPStan Level 6 Fixes
* QA: Fix lib/xml.php to PHPStan Level 6
* QA: Wrong grep -v
* PHPStan Level 6 for lib/vdef.php
* PHPStan Level 8 for lib/time.php
* QA: Raise PHPStan to Level 6 for lib/database.php
* QA: Remove @deprecated tag from grv()
* QA: Switch to htmle() alias over html_escape()
* QA: More addressing PHPStan issues at Level 6
* QA: Change recommended by Copilot for Readability
* QA: More PHPStan fixes
* QA: More PHPStan fixes
* QA: More PHPStan fixes for lib/utility.php
* QA: More PHPStan fixes at Level 6
* QA: Raise PHPStan to Level 6 More Functions
* QA: Raise PHPStan to Level 6 More Functions
* QA: Raise PHPStan to Level 6 More Functions
* QA: Raise PHPStan to Level 6 More Functions
* QA: Raise PHPStan to Level 6 More Functions
* QA: More PHPStan fixes at Level 6
* QA: Raise PHPStan to Level 6 More Functions
* QA: Raise PHPStan to Level 6 More Functions
* QA: More PHPStan fixes at Level 6
* QA: More PHPStan fixes at Level 6
* QA: Raise PHPStan to Level 6 More Functions
* QA: Raise PHPStan to Level 6 More Functions
* QA: More PHPStan fixes at Level 6
* QA: Raise PHPStan to Level 6 More Functions
* QA: Raise PHPStan to Level 6 More Functions
* QA: More PHPStan fixes at Level 6
* QA: Raise PHPStan to Level 6 More Functions
* QA: Raise PHPStan to Level 6 More Functions
* QA: More PHPStan fixes at Level 6
* QA: More PHPStan fixes at Level 6
* QA: More PHPStan fixes at Level 6
* QA: Raise PHPStan to Level 6 More Functions
* QA: More PHPStan fixes at Level 6
* QA: Some phpseclib3 fixups to update to latest
* QA: More fixups for graphing for magic strings
* QA: More PHPStan fixes at Level 6
* QA: Fixing order string type on get_allowed_graphs()
* QA: Fixing improper use of print
* QA: Optimization of color_templates
* QA: Copilot recommendation
* QA: Copilot recommendation
* QA: Copilot recommendation
* QA: Copilot recommendation
* QA: Copilot recommendation
* QA: Copilot recommendation
* QA: Copilot recommendation
* QA: Copilot recommendation
* QA: Raise PHPStan to Level 6 More Functions
* QA: Issues insalling Cacti
* QA: Issues insalling Cacti
* QA: Issues insalling Cacti
* QA: Issues insalling Cacti
* QA: Issues insalling Cacti
* QA: Issues testing Cacti
* QA: Copilot recommendation
* QA: More PHPStan fixes at Level 6
* QA: More PHPStan fixes at Level 6
* QA: More PHPStan fixes at Level 6
* QA: More PHPStan fixes at Level 6
* QA: More PHPStan fixes at Level 6
* QA: Installer failing
* Undo ownership changes to remove tar errors
* QA: Updates after php-cs-fixer
* QA: Adding code checks to syntax checking
* QA Change syntax flow language
* QA: Reduce the number of cures
* QA: Use sequentional option
* Change runner to self-hosted for check job
* Fix typo in OS version in workflow file
* Add Git installation step in workflow
Added step to install Git if not already installed.
* Add '-y' flag to apt-get install and update commands
Updated apt-get commands to include '-y' for automatic confirmation.
* Change workflow OS version to 'ubuntu-latest'
Updated the workflow to use 'ubuntu-latest' instead of 'ubuntu:latest' and modified the 'runs-on' configuration.
* Remove sequential option from phpcsfixer script
* Remove PHPCS fixer step from workflow
Removed PHP coding standards check from workflow.
* Move code checks to before plugins
Moving these core checks to before the plugins are installed as we really want to check only the base Cacti here.
* Add Composer validation and caching to start of workflow
* QA: Addressing PHPStan additional
* QA: Restore composer lock file
* QA: Addressing PHPStan additional
* QA: Addressing PHPStan additional
* QA: Complete LDAP Project
* QA: php-cs-fixer doing it's thing
* QA: Syntax is already happening in PHPStan
* QA: The md5 function no longer exists
* QA: Update after ldap changes
* QA: Undo that last password hack
* QA: Attempting to switch to MariaDB 11.8
* QA: Attempting to switch to MariaDB 11.8
* QA: Resolving some issues with PHP8.4
* QA: Resolving some issues with PHP8.4
* QA: Add PHP8.5 to the matrix
* QA: Removing PHP8.5 due to dependency issues
* QA: Add the DB version to the testing output
* QA: Switch back to MySQL for the moment
* QA: More fidelity in reporting
* QA: The pipe must be mixed
* QA: Moving audit_schema.php changes from 1.2.x
* QA: Moving audit_schema.php changes from 1.2.x
* QA: Restore mysql monitoring
* QA: php-cs-fixer issues
* QA: Attempt to roll back to MariaDB 10.6
* QA: Attempt to fix check_all_pages.sh
* QA: Make installer functional again
* QA: Fix installer php-cs-fixer issues
* QA: Type to a string
* QA: Get the database version correctly
* QA: Fix issue with reports in check_all_pages.sh
* QA: Check the server version in the right place
* Feature: Support SHA256 as a new hash for passwords
* QA: Function definition and return typing audit
* QA: Refinement of form types
* QA: Refinement of form types
* QA: Consistency of comments
* QA: Issues with php-cs-fixer and more comments
* QA: More typing issues
* QA: php-cs-fixer on merge conflict resolution
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This removes most entries to $config in the base code. A few new constants were defined to achieve this:
* CACTI_CONNECTION - The status of the current connection
* CACTI_CSRF_SECRET - The path to the csrf secret file
* CACTI_PHP_SNMP - True if php-snmp is installed
* CACTI_WHITELIST - The path to the Cacti Whitelist file if defined
I also moved several $config['poller_id'] to the existing POLLER_ID and move a number of $config['is_web'] to the existing CACTI_WEB constants.
There remain a few artifacts in the code. They will be handled in a separate commit.
* Add Rector
* Add Rector
* Fix "Cashed" misspelling
Also harmonize the phrase with the one below it
* Fix implode error in db_table_create
Only relevant when multiple primary keys
* Fix auth issue, parentheses in the wrong location
Was causing too many arguments to be sent to cacti_sizeof which only take 1, instead of get_allowed_devices.
* Fix issue with session_var not properly using class variable
* Fix pass by reference error in get_allowed_devices
get_allowed_devices has a pass by reference on the "total_rows" field. It is used in several places to pass the total_rows back to a counter. Here though, we are passing a number directly to the function, which errors out with "Uncaught Error: get_allowed_devices(): Argument #4 ($total_rows) cannot be passed by reference". So we will just place a dummy variable in front to catch the pass.
This wasn't caught before because the previous parentheses issue.
* Fix undefined variable "field_array" in html_filter
* Fix undefined function 'get_nfilter_filter_request_var'
* Fix undefined variable 'resizable'
I corrected the function parameter, as that was technically the incorrect spelling
* Fix invalid variable $user_id
* Fix undefined vars in spikekill class
* Fix copy and paste error
Oops
* Forgot to use the dummy variables
* Add missing $
Oops
* Start converting to newer PHP Syntax and Functions
* Test fixing the workflow by install composer
* Lets not valid the composer file right now
Seems we have a few things to fix it in
* Validate composer
* Lite formating changes
* lite formatting
* lite formatting
* lite format changes
* lite format changes
* lite format changes
* lite format changes
* lite format changes
* lite format changes
* lite format changes
* lite format changes
---------
Co-authored-by: TheWitness <thewitness@cacti.net>
This is tricky, and still breaks down if the second line has an equal sign. For example:
.1.3.2.4 = Some Text\n
Some More Text = Stuff\n
This is a real long line.
.1.3.2.5 = Some numeric value
So, it's not perfect.
- Issue with SNMP Bulkwalk Repetitions Size
- This enhancement adds a new column to the host table for bulk walk size us
- Detection can happen on each re-index, set manually, or detected and set one time.
- Changed Max OID's to a dropdown
- Max Repetitions is also a dropdown now
- This setting was also carried to the Automation portion of Cacti and the Add Device CLI