* fix(boost): harden data handoffs and cache writes
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Define $updates_ok when there is nothing to flush
PHPStan level 6 also flags the poller ownership check, which is the POLLER_ID
constant case the file already annotates above.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Use a line comment to match the fixer
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Record the boost process columns in the audit schema
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: resolve includes through Cacti path constants
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Fix Boost handoff review concerns
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: seed settings in Boost database fixture
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: refresh SQL interpolation baseline
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* docs: record Boost handoff hardening
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix: stop boost discarding a cycle and wedging a shard
Three defects in the handoff hardening.
A rejected ownership handoff returned false, which the poller reads as boost
having consumed the data, so it skipped the RRD write. The poller_output rows
were already deleted by then, so a stale poller_item copy on a remote poller
silently lost the whole cycle. Return true instead and let the poller write
directly, matching the database failure path.
Any non-OK RRDtool response retained the shard, and the retry then replayed rows
already written, which RRDtool rejects as an illegal update time. That failed
again on every pass, so the archive tables accumulated and boost never drained.
Treat an at-or-behind timestamp as applied, since the sample is in the RRD;
genuine errors still retain the shard.
The two new poller_output_boost_processes columns passed 'null' where
db_install_update_table reads 'NULL', so NOT NULL was never emitted and upgraded
installs diverged from cacti.sql.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix: spell the NULL column key so upgrades emit NOT NULL
db_add_column() reads $column['NULL'] and PHP array keys are case sensitive, so
the thirteen definitions in the 1.3.0 upgrade that passed a lowercase 'null' key
had the flag discarded and created nullable columns. Fresh installs built from
cacti.sql got NOT NULL, upgraded installs did not, and Cacti's own schema audit
reports the difference.
The same file already spells the key in upper case 144 times, and the wrapper's
own docblock example uses the upper case form.
Guard the class with a test over every upgrade file rather than only the one
that was wrong.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: assert the NULL key plainly
toContain() takes needles rather than a failure message.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): close remaining handoff review gaps
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): guard nullable graph output
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): keep in-flight completion rows through the 1.3.0 upgrade
The truncate only existed so the run_child UNIQUE key could build over rows
that predate run_id; deleting just those rows leaves a running boost its
completion records.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* refactor(boost): drop the poller ownership gate
Its boost_poller_on_demand() call sits behind the boost_redirect early return
that poller.php forces on for every multi-collector install, and its recovery
call turns a device deletion on main into a recovery that never brings the
collector back online.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): bound atomic cache write allocations
Guard upgrade source reads so failures report clearly, write graph cache data in fixed-size chunks, and exercise a multi-chunk payload.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): support Windows cache replacement
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test(boost): expand handoff failure coverage
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test(boost): support ANSI_QUOTES upgrade checks
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): stop staging after chunk failure
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(recovery): use numeric poller status
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(sql): support ANSI_QUOTES in recovery paths
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): preserve cache on replacement failure
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): retain readable cache permissions
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): repair completion schema at runtime
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(recovery): bound timestamp batch fetches
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): satisfy runtime schema analysis
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(recovery): restore status after transfer failure
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test(boost): rely on unit bootstrap helpers
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): refresh generated cache secret
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(boost): normalize cache path separators
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test(boost): fail clearly on unreadable fixtures
* fix(boost): name ignored duplicate sample keys
The staging warning only said rows were dropped, so a retry could not be told from a colliding writer.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Lack of pre-commit hook execution allowed lint failure
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Co-authored-by: TheWitness <thewitness@cacti.net>
* chore: replace removed jQuery $.isFunction() with typeof checks
jQuery 4.0 removes $.isFunction(). Swap the three call sites in navigationBox.js
and navigationBox.tree.js for behaviour-identical 'typeof x === function' checks,
removing the only jQuery-4-removed API in Cacti's own front-end code.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix: correct typeof negation in the mark.js availability check
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(poller): stop the registry pid probe narrowing a wide pid to -1
processes.pid is int(10) unsigned, so a corrupted row can hold 4294967295.
Through PHP 8.4 posix_kill() narrows that to a 32 bit pid_t of -1 and kill(2)
reads it as every process the caller may signal, so cacti_process_pid_exists()
calls a dead registry row live and the two kill sites that ask it first SIGTERM
the caller's whole session. From PHP 8.5 the same value raises a ValueError and
ends the poller instead. Refusing it ahead of the call covers both.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* hardening: bound every process table PID before signalling it
Twelve call sites read a pid out of a process table and handed it straight to
posix_kill(). processes.pid is int(10) unsigned, so a corrupted row can carry
4294967295, which narrows to a 32 bit pid_t of -1 and signals every process
the caller owns, or from PHP 8.5 raises a ValueError that ends the collector.
They now go through cacti_process_kill(), which refuses init, anything that
cannot name a process, and anything wider than pid_t.
poller_recovery read its pid with a bare posix_kill($pid, 0) and treats false
as "stale, clear the row and run". cacti_process_signalable() keeps exactly
that meaning, including EPERM as stale, and adds the bound. Neither
cacti_process_pid_exists() nor cacti_process_still_running() fits there: both
report a pid live when signalling it is merely forbidden, so a recycled
recovery_pid would have wedged recovery until an operator cleared the row.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: place process guards in poller domain
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(poller): honor platform PID ranges
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test(poller): cover platform signal semantics
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(csrf): require POST tokens for item actions
* test(csrf): cover every guarded action path
* test(csrf): keep legacy guard contract extensible
* fix(csrf): hide POST-only targets from crawlers
* test(csrf): reject every crawlable state-action link
* fix(csrf): fail closed without logging crawler probes
* fix(csrf): post data-query item actions safely
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(csrf): normalize stripped token separators
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(csrf): distinguish method and token failures
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(csrf): escape POST action URL attributes
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(csrf): delegate POST action events
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(csrf): post data template add action
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* style(html): align POST button class
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* feat(cli): add unified command frontend and normalize legacy failures
Add the Symfony-backed bin/cacti entry point while preserving legacy argument, terminal, signal, and exit-status behavior. Fold in the focused legacy CLI correctness fixes from #7906 without adding its unused parser abstraction.
Keep path containment and the PHP 8.3 floor out of this review; the latter remains in #7939.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(cli): enforce PHP floor and deny web dispatch
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* build: raise the 1.3 floor to PHP 8.3
RHEL 10 ships 8.3 in AppStream and dropped modularity, so an 8.4 floor would
put every RHEL, Alma and Rocky 10 install on Remi. Ubuntu 24.04 is 8.3 as
well, which makes it the lowest common denominator of the current LTS fleet.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* docs: raise the documented PHP floor to 8.3
The 1.2.x column keeps 8.1, which still matches that branch's composer constraint.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* ci: run process lock coverage on PHP 8.3
* fix: finish PHP 8.3 floor cleanup
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7809: stop reading a missing source key on output-only data query fields
query_snmp_host() tested $field_array['source'] as the left-most term of the
method-fix guard, so PHP raised "Undefined array key source" for every
output-only field before the direction test could short-circuit. Testing
direction first reads source only for input fields, which always define it.
The boolean result is unchanged.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
(cherry picked from commit 5f25145ba0)
* docs: restore changelog issue ordering
* Tighten data query regression error handling
* Clarify data query regression comment
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
process-only was empty, so lock-threads walked discussions as well, and the
job has no discussions permission. The exempt lists also matched only the
flat label names, which would have exposed 238 open enhancements to the
stale closer once those labels are retired.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Serialize process registry admission with Symfony Lock
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Fix PDO process lock mode handling
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* ci: run process lock coverage on PHP 8.2
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix: confirm a pid is still ours before signalling it
#7574 added the still-running guard to dsstats but left six byte-identical
siblings signalling a pid straight out of the processes table. After a crash or
a power loss the row survives, the pid is recycled by an unrelated daemon, and
the next start SIGTERMs a stranger. That is issue #7425, still live in rrdcheck,
boost, poller_commands, float_rrdfiles, poller_reindex_hosts and
rebuild_poller_cache.
Wrap each kill in cacti_process_still_running(), matching the dsstats shape, and
leave unregister_process() outside the guard so a stale row is still cleaned up.
Cover all seven with a test that walks every kill site, so a new variant cannot
reintroduce it.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: assert the guard plainly
toContain() takes needles rather than a failure message, so the file name was
being asserted as a second required substring and the test always failed.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* docs(poller): describe required lock parameters
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Thomas Vincent <thomasvincent@gmail.com>
* feat(notify): let one notification address several recipients
Symfony calls a channel once per recipient, so a caller that needs a single
message sent to several people could not express it. The email 'to' option
folds the extra addresses into the same mailer() call and drops any that
match the recipient.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(notify): compare the local part of an address exactly
RFC 5321 leaves local part case to the receiving host, so folding the whole
address deduplicated OPS@example.com and ops@example.com into one mailbox.
Only the domain folds now.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* security(cli): keep database credentials off the command line
audit_database.php passed the database password as --password= to the mysql
client. Escaping does not help there: an argument is readable from the process
list by every local user for as long as the import runs, and with --debug the
same string was printed to stdout. The credentials now go in a defaults file
created with mode 0600 and removed afterwards.
splice_rrd.php named its dump files from the RRD basename and mt_rand() in a
world writable directory and created them by shell redirection, and
backupRRDFile() copied into the same place. Both now use one private directory
created for the run. Modern kernels block the symlink case through
fs.protected_symlinks, so this is hardening rather than a fix for an
exploitable path, but the names should not be guessable in the first place.
float_rrdfiles.php appended its debug log to a fixed /tmp/clearer.log, which
any local user could pre-create or read. It now uses the system temporary
directory with a name of its own.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* ci: refresh audit database checks
* fix: quote credentials written to the MySQL option file
The option file format is not byte transparent. An unquoted '#' starts a
comment, a backslash escapes, and trailing whitespace is stripped, so any
install whose database password contained one of those authenticated with a
truncated credential and failed the schema load with no useful diagnostic.
Double quote the values and escape the backslash and quote characters,
verified against my_print_defaults.
Refuse a credential containing a newline rather than write a file the parser
would read as further options.
Use --defaults-file rather than --defaults-extra-file so a ~/.my.cnf belonging
to whoever runs the script cannot override the credentials Cacti supplies, and
trim the 'which mysql' fallback, whose trailing newline split the command and
stranded the credential argument on the second line.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Align CLI hardening with quality guards
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
Three things that cost real time and are not written down anywhere.
The sink and hotspot guards compare against checked-in baselines and gate every
pull request, but the refresh command is only printed by the verifier on
failure. A branch can go red on its own guard when a merged change moves a sink,
and every pull request then inherits the failure. Record the command, and note
that the verifiers strip the line number before comparing so most of the diff is
renumbering they ignore.
A defect reported against one branch is often already fixed on the other, in one
case by a pull request that had merged. Check the target still exists before
writing the fix.
A pull request whose workflow runs are held in action_required reports no checks
at all, which reads as passing. Say plainly that zero checks means unverified.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* ci: share one body across the per-script coverage workflows
Eight of the coverage workflows were the same file with four things changed:
the PHPUnit configuration, the job name, occasionally the PHP version, and
occasionally the snmp extension. Everything else, the triggers, permissions,
concurrency, checkout, dependency setup and the Pest invocation, was copied
between them, so a change to any of it meant eight edits and eight chances to
miss one.
They now call a reusable workflow and supply only what differs. Each caller
keeps its own path filters and concurrency group, since those are genuinely
per-script.
installer, snmp and rrd-graph-item are left alone. They run more than one
suite or produce clover output, so folding them in would mean parameterising
the shared body until it stopped being shared.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* ci: rebuild the callers when the shared coverage body changes
Each caller watched its own file but not the reusable one, so editing the
shared body would not have run any of the suites it drives.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
There was no root CONTRIBUTING.md and no way to run the CI gates locally in one
step, so reproducing a CI failure meant reading workflow YAML. Add a
'composer check' target that chains the gates, expose the sink and hotspot
guards, the changelog validator and the JavaScript tests as their own targets,
and document setup and the changelog.d fragment workflow.
Pin Node in .nvmrc to the version CI uses, and move the one remaining
setup-node still on 20 to 22 so every workflow agrees.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* build: analyse at PHPStan level 8 and unpin the analyser
The level lived in the composer script as --level 6 while .phpstan.neon
declared none, so anyone running phpstan directly analysed at level 0 and saw
nothing. The level now sits in the config where both CI and a developer get
the same answer.
Level 8 finds 2787 issues on the current tree, so the baseline is regenerated
rather than the debt fixed here. The gate passes on what exists and fails on
anything new, which is the point: a fresh offset access on a db_fetch_row()
result is now caught in review instead of at runtime.
The debt is not 2787 unrelated problems. 1556 of them are the same shape,
"Cannot access offset X on array|bool", from the db_fetch_* family declaring
bool|array and call sites indexing the result without testing it. Narrowing
those return types would clear more than half in one change, which is worth
doing on its own.
phpstan was pinned to an exact 2.2.9, so patch releases were not reaching CI.
It now floats on ^2.2.12.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* build: refresh the PHPStan baseline against current develop
The baseline was generated while phpseclib was temporarily pinned back to the
3.x line. develop has since migrated the call sites to phpseclib4, so three
entries no longer match anything.
reportUnmatchedIgnoredErrors is false in .phpstan.neon, so stale entries are
ignored rather than reported and would have sat there indefinitely. The
migration introduced no new level 8 findings: 1483 entries become 1480, with
nothing added.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* build: refresh PHPStan baseline after rebase
* build: refresh baseline after transaction fix
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7492: use tempnam for import temp files instead of rand()
The import wrote to sys_get_temp_dir()/package_import_<rand()>, a predictable
name a local user could pre-create or symlink. tempnam() creates the file
atomically with an unpredictable name.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7451: sanitize the CactiTableFilter search term
The shared table-filter Search box registered its term with FILTER_DEFAULT
(no sanitization), so the raw term reached filter processing. Validate it with
sanitize_search_string like the other search filters.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7450: confine the auth-none change-password referer redirect
check_reset_no_authentication() placed the raw HTTP_REFERER into the
auth_changepassword ref= parameter, an open redirect and header-injection
vector. Wrap it in urlencode(validate_redirect_url()), restoring the guard
1.2.x already has (this regressed on develop).
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7490: anchor the graphs.php template_id filter regex
The template_id filter used an unanchored regex, so any value containing a
cg_/dq_/digit substring passed validation and reached explode()/SQL. Anchor it
to match the hardened sibling filters in lib/html_graph.php.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7453: escape template and query names in the package preview
get_package_contents() escaped file names with htmle() but emitted graph
template, data query, and data-query graph-template names raw, so a package
whose template name contains markup produced stored XSS in the import preview.
Wrap the names in htmle() like the file rows.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: refresh sink inventory baseline
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* security: substitute query data before escaping graph titles and labels
rrd_substitute_host_query_data() ran over the whole graph command after each
argument was cacti_escapeshellarg'd, so a device-supplied SNMP value expanded
from a |query_*| token in a graph title, vertical-label or right-axis-label
could contain a quote and break out of the RRDtool argument. Move the
substitution inside cacti_escapeshellarg at those three fields and drop the
post-escape whole-command pass. Forward-port of the 1.2.x fix.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* security: finish resolve-before-escape refactor, drop redundant inner substitution
$value is already resolved (credential-stripped and host/query substituted) by
rrdtool_resolve_graph_text() at the top of the graph-options loop, so the inner
rrd_substitute_host_query_data() in the title, vertical-label and right-axis-label
argument builders was a redundant second pass. Reconcile the older escape test to
match; the device-quote-neutralisation behavior test is unchanged.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix: commit transactions on MySQL as well as MariaDB
db_commit_transaction() gated the commit on SELECT @@in_transaction. That
variable is MariaDB only; MySQL answers 'Unknown system variable
in_transaction', so the guard was false on every MySQL install and the commit
never ran. The transaction was left open and the function reported failure.
Verified against both engines: MariaDB returns 0, MySQL 8 raises ERROR 1193.
Every caller was affected, including device removal, which reports 'Unable to
remove Device N because its database transaction could not be committed' and
leaves the device in place.
Use PDO::inTransaction(), which tracks the state itself and answers the same
question on both engines.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Update transaction regression assertion
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.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>
The recursive wget crawl is the single most expensive thing in CI. Measured over
the last 120 runs, the Commit Audit workflow is half of all CI minutes, the
locked integration job is 72 percent of that workflow, and this one step is 60
percent of that job: 422 seconds of a 16 minute run.
It walks the authenticated UI one request at a time, so nothing it covers can
change unless server-rendered code, assets or the schema did. Skip it when a
pull request touches only tests, workflows, translations or documentation.
The step is gated rather than the job, so the required-check aggregator still
sees a real result rather than a skip. The decision fails open: an unreadable or
empty diff, or a push rather than a pull request, runs the crawl.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* chore: manage front-end JavaScript libraries with npm and Dependabot
Move all third-party front-end JavaScript to npm so Dependabot tracks it, and
drop the committed copies in favour of a build. package.json pins jquery,
jquery-ui, jstree (3.3.12 -> 3.3.17), billboard.js, pace-js, tablesorter and
jquery-validation; build/sync-js.mjs copies their dist files into include/js/
and runs on npm install via postinstall.
pace-js carries a one-line local fix (a stray regex alternation that made it
match the empty string) applied through patch-package
(patches/pace-js+1.2.4.patch); the fix should be sent upstream so the patch can
be dropped. Only Cacti's own themed jquery-ui/jstree CSS stays committed.
An npm ecosystem is added to dependabot.yml for develop. The release pipeline
and any workflow that serves Cacti pages must run 'npm ci' (which builds via
postinstall) before use, since the generated files are no longer in the tree.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* hardening: remove vendored development artifacts
* feat(release): attach the built artifact to the release
The workflow already builds an attested, checksummed archive carrying the
Composer dependencies, then discards it after thirty days.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* ci: fail the publish step when the bundle is empty
An unmatched glob would otherwise reach gh as a literal path.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* ci: treat the maintenance lock notice as expected log output
The line records the lock working, so a second maintenance run overlapping
in CI failed unrelated pull requests.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* docs: consolidate release dependency changelog
* ci: build managed JavaScript in release artifacts
* test: cover managed JavaScript build end to end
* deps: complete phpseclib 4 migration
* deps: update patch-package to audited release
* ci: satisfy workflow shell policy
* chore: source DOMPurify from npm and update it to 3.4.14
Add dompurify to the npm-managed set (was committed at 3.4.7) and bump to 3.4.14.
DOMPurify is the client-side XSS sanitizer used on tree labels, device tooltips
and plugin messages; keeping it current with upstream bypass fixes matters more
than for the UI libraries.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* docs: preserve release changelog ordering
* test: preserve DOMPurify source map in npm build
* chore: source htmx, d3, big.js, lzjs and screenfull from npm
Add the remaining unmodified third-party libraries to the npm-managed build:
htmx.org 2.0.6, d3 7.8.2, big.js 6.2.2, lzjs 1.3.1 and screenfull 3.3.3. Each
was verified byte-identical (lzjs differs only in a JSDoc comment) to the copies
being removed. htmx in particular moves from a hand-tracked SHA pin to npm with
lockfile integrity.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: cover phpseclib 4 integration
* ci: build managed assets before htmx tests
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7024: do not delete still-running poller_time rows on cycle start
The overrun path deleted every poller_time row for the poller, including the
in-flight ones it had just counted, so overrunning processes lost their
tracking. Restrict the delete to completed rows.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7028: restore identifier and connection safety in the db layer
_db_replace interpolated caller-supplied column keys as backtick identifiers
without escaping, and db_commit_transaction checked @@in_transaction on the
default connection rather than the one passed in. Both were correct on 1.2.x
and regressed on develop.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7341: size the CSV export maxrows to the requested span
The export hardcoded --maxrows=10000, truncating wide date ranges regardless
of the data source profile step.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7421: offset automation_get_next_host through the 32-bit integer
The hand-rolled octet walk subtracted the wrong magnitude per octet and
emitted out-of-range octets for ranges wider than a /16.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7030: skip the poller_output ENGINE=MEMORY conversion when already applied
The output-table refresh ran ALTER TABLE poller_output ENGINE=MEMORY every
cycle even though the swapped-in copy already inherits the engine, adding a
metadata-locking DDL to the hot path. Probe the current engine and convert
only when needed.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7342: prefer snmpEngineTime only when it covers system uptime
update_system_mibs preferred snmpEngineTime (x100) over sysUpTime whenever it
was present, so an SNMP engine that restarts independently of the system (for
example OpenBSD snmpd) made the stored uptime drop, which reads as a reboot
and forces a perpetual reindex with mass 'U' results. Capture both values and
use snmpEngineTime only when it is at least the system uptime, which still
covers the 32-bit sysUpTime wrap.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7403: scale lmsensors readings in the get path like the query path
The get path returned the raw milli-unit SNMP counter while the query path
divided voltage and temperature by 1000 (and corrected negative voltages), so
a get gave values 1000x too large. Apply the same scaling before returning.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7029: propagate the reconnected handle back to the caller
db_check_reconnect() closed the dead connection, opened a new one, and
returned true without giving the new handle back, so the db_execute_prepared
retry ran its query against the connection that was just closed. Take $db_conn
by reference and assign the fresh handle; the one caller that passed a literal
false now passes a variable.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: refresh sink inventory baseline
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: refresh architectural hotspots baseline
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7474: return valid after correcting the theme to selected_theme
is_valid_theme() corrected an invalid theme to selected_theme but left $valid
false, so callers saw the theme as invalid even though it had been fixed.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7462: allow SELECT aliases as sortable automation columns
site_name and host_template_name are aliases (s.name AS site_name), valid in
ORDER BY but not real columns, so api_automation_column_exists() rejected them
and the site-name sort silently failed.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7456: bind the OS value in the automation OS filter
The OS filter built 'os = ?' but bound the network value, so filtering by OS
matched on the wrong input.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7491: filter host_template_id against its own column
html_make_device_where() compared h.location to the host_template_id value
instead of h.host_template_id.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7479: apply the hostname and description filters in get_hosts
The REST get_hosts() honored host_id/poller/site/template/location but ignored
the hostname and description filters its callers pass; add them as bound LIKE.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7475: authorize a direct client-IP poller match before the hostname guard
A poller configured by IP with no hostname-based pollers was rejected because
the empty-hostname guard ran before the direct-match check, even though the
client address exactly matched a poller's configured address.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Guard two divisors that PHP 8 makes fatal
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* docs: consolidate functional fix changelog
* test: cover consolidated functional fixes
* docs: identify consolidated functional fixes
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7355: add a vendor-neutral ENTITY-SENSOR-MIB script data query
ENTITY-SENSOR-MIB (RFC 3433) exposes physical sensors (temperature, fan, PSU,
voltage, current) on most modern gear, so one data query covers F5, Juniper,
Arista, Cisco NX-OS and others. ss_entity_sensor collects the sensor table and
scales each reading through entPhySensorScale and entPhySensorPrecision; the
SNMP I/O sits behind an injectable walker so every logic branch is unit tested.
A dedicated coverage job enforces 100% line coverage on the collector.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7355: add a JUNIPER-MIB jnxOperatingTable script data query
Junos routers and switches expose per-subject health (Routing Engine, FPC, PSU,
fan) through JUNIPER-MIB jnxOperatingTable, which is the signature Juniper data
beyond the IF-MIB and HOST-RESOURCES standards. ss_juniper_operating collects
CPU, temperature, buffer and state; the multi-index table is keyed on the full
OID suffix. SNMP I/O sits behind an injectable walker so every branch is unit
tested, and a dedicated job enforces 100% line coverage.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* issue#7355: add an F5-BIGIP-SYSTEM-MIB per-CPU utilization script data query
BIG-IP reports per-CPU utilization as sliding 5s/1m/5m averages in
sysMultiHostCpuTable, the signature F5 metric beyond the IF-MIB and
HOST-RESOURCES standards. ss_f5_cpu collects that multi-indexed table, keyed on
the full OID suffix. Every OID was resolved from the published
F5-BIGIP-SYSTEM-MIB with snmptranslate. SNMP I/O sits behind an injectable
walker so every branch is unit tested, and a dedicated job enforces 100% line
coverage.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* style: apply php-cs-fixer to ss_entity_sensor.php
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
The verifier chose SHA-1 for short public keys, so a package signed with the
weak, forgeable SHA-1 was accepted. Always verify with SHA-256.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* security(import): validate Data Source item fields coming from Template XML
data_source_name, rrd_minimum and rrd_maximum reach an RRDtool command line
but never pass through the data_sources.php form on the import path, so the
same field rules are applied at the XML handoff instead.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* chore: add the CHANGELOG entry
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(import): name the rejection and keep the value out of the log lines
Code 7 reads as an XML parse error, which this is not, and the value comes
from the file being imported.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* style: use a line comment to satisfy php-cs-fixer
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: resolve includes through Cacti path constants
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* ci: restore changelog entry ordering
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(changelog): order the 7720 issue entry before features and drop a duplicate
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Use Symfony Finder for MD5 traversal
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* chore: add the CHANGELOG entry
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix(md5sum): reject a bad ignore pattern instead of ignoring every file
preg_match() returns false on a bad pattern and false is not 0, so the old
test skipped the whole tree and wrote an empty manifest.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* style: use a line comment to satisfy php-cs-fixer
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: resolve includes through Cacti path constants
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test(md5): enforce full finder coverage
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Move Finder import before CLI bootstrap
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* build: lock Symfony Finder runtime dependency
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* build: leave Composer autoload metadata generated
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* ci: run Finder coverage on PHP 8.2
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* fix: widen the symfony/finder constraint for PHP 8.5
Pinning ^6.4 left the dev dependency graph unsatisfiable on PHP 8.5 in the native-dependencies job.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* Add fail2ban support for authentication failures
Emit a fixed, machine-parseable AUTH FAILURE line (user, realm, ip, reason)
from the universal login-failure path and the 2FA branch, normalise the human
AUTH strings, and ship a fail2ban filter, jail example, and README. The IP
comes from get_client_addr(); a crafted username cannot forge fields.
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
* test: resolve includes through Cacti path constants
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
---------
Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>