Cacti is a robust, extensible network monitoring and graphing tool built on the LAMP stack.
-
Core Purpose: Polls devices (SNMP, scripts), stores time-series data in RRDtool, and renders historical graphs for network operations.
-
Architecture:
- Frontend: PHP 8.3+ web UI for configuration and visualization.
- Backend: MySQL/MariaDB for metadata/state/configuration/statistics; RRD files for metrics.
- Polling: Scalable data collection via
cmd.php(PHP) orspine(C), supporting remote pollers.
-
Key Components:
- Templates: Abstraction for Graphs, Data Sources, and Devices to simplify management.
- Plugins: Extension system for adding features (e.g., Thold, Weathermap).
- Automation: Rules for discovering and adding devices automatically.
- Web: Pages start with
include/auth.php(loadsinclude/global.php, enforces auth). - CLI: Scripts load
include/cli_check.php(enforces CLI context). - Polling & Data Flow (Principles of Operation):
- Hierarchy: Sites > Data Collectors > Devices > Graphs.
- Collection:
poller.php(cron) orcactid.php(daemon) fetch data via SNMP,script_server.php(resident PHP pool), or external scripts. - Storage: Metadata in MySQL/MariaDB; Metrics in RRDfiles (
.rrd). - Visualization: RRDtool renders graphs (see
graph_image.php).
- API: REST endpoints in
api/(Slim framework). - CLI Scripts: Administrative scripts located in
cli/ - SNMP Query Templates: XML templates with snmp OID's to poll located in
resource\snmp_queries
- Coding Style:
- Indentation: Tabs (4 spaces width).
- Braces: Start on the same line (
if (...) {). - Quotes: Single quotes
'preferred over double"unless interpolating. - Constants:
true,false,nullshould be lowercase. - Arrays: Use
foreachoverwhile(list() = each()).
- Database (MariaDB/Mysql):
- Use helpers in
lib/database.php; prefer prepared calls likedb_fetch_row_prepared()/db_fetch_assoc_prepared()/db_execute_prepared(). - Do NOT quote numeric values in SQL. Use single quotes for strings.
- Always use
ASfor aliases. - Use explicit
INNER JOIN,LEFT JOIN, etc. (no implicit joins).
- Use helpers in
- Request handling (web):
- Avoid direct
$_GET/$_POST/$_REQUEST. - Use
get_request_var()/get_filter_request_var()andform_input_validate(). - Validation: Use
validate_store_request_vars()where possible. - Request variable caching (aliases in
lib/html_utility.php):gfrv($name)/get_filter_request_var()— validates input (default: integer viafilter_var), stores the sanitized value in$_CACTI_REQUEST, and returns it.grv($name)/get_request_var()— retrieves from$_CACTI_REQUESTcache. Returns pre-validated data ifgfrv()was called first for that variable.gnrv($name)/get_nfilter_request_var()— retrieves from cache without filtering. Safe ifgfrv()was called first.srv($name, $value)/set_request_var()— stores a value in$_CACTI_REQUEST,$_REQUEST,$_POST, and$_GET.- Do NOT flag
grv()/gnrv()calls as unsanitized whengfrv()was already called for that variable in the same request. The validation happens once at thegfrv()call site; subsequent retrievals return cached, pre-validated data. - Adding redundant
(int)casts or parameterization aftergfrv()has validated is unnecessary.
- Avoid direct
- Common page flow:
set_default_action(); switch (get_request_var('action')) { ... }and render viatop_header()/bottom_footer(). - CSRF: AJAX posts include
__csrf_magic: csrfMagicToken. - Logging: use
cacti_log(...)andcacti_log_file(). - i18n: wrap UI strings with
__('...').- Translatable strings are managed with GNU gettext.
locales/po/cacti.potis the source template; Weblate owns syncing the per-language.po/.mofiles. - Any PR that adds, changes, or removes a
__('...')(or__n()/__esc()etc.) string MUST updatelocales/po/cacti.potin the same PR. This is mandatory — a PR that touches translatable strings without updatingcacti.potis incomplete. Removing a string counts: it must be dropped from the template too. - Never commit the per-language
.poor compiled.mofiles —locales/po/cacti.potis the only translation artifact a PR may add or modify. Stage only that file. - Regenerate with
locales/update-pot.sh(it runsxgettext), or, to keep the diff focused, add/remove just the affectedmsgidentries. Validate withmsgfmt --check-format -o /dev/null locales/po/cacti.pot. - If
update-pot.shrewrites.po/.moside effects, revert them (git checkout -- locales/po/*.po locales/LC_MESSAGES) so the PR touchescacti.potonly.
- Translatable strings are managed with GNU gettext.
- Plugins:
- Hooks via
api_plugin_hook(...)inlib/plugins.php. - Must have an
INFOfile (INI format). - Use
top_header()/bottom_footer()(no direct includes). - Add
&header=falseto URLs for AJAX requests.
- Hooks via
- CHANGELOG:
- Within each release section, keep
-issueentries first and-featureentries after them. - Append new issue entries at the end of the issue block and new feature entries at the end of the feature block.
- Keep numbered entries in their existing numeric order; every new entry must include an issue or pull-request number (
#1234). - Preserve the existing entry prefixes, release headings, and blank-line spacing.
- Within each release section, keep
- URLs and redirects:
- Use
cacti_url($path, $params)for RFC 3986 query-string construction, including URLs with fragments. - Use
cacti_redirect($path, $params)for local redirects; it rejects absolute and protocol-relative destinations (checking both raw and percent-decoded forms) and terminates the request. - Escape output for HTML attribute contexts with
html_escape_attr(), nothtmle()/html_escape(); only the attribute helper double-encodes to stop pre-encoded entities from breaking out of the attribute.
- Use
Keep maintenance fixes focused on the reported defect and preserve branch compatibility.
- Before adding persistent caches, staging tables, locks, shutdown handlers, or invalidation hooks across management APIs, show why the existing mechanism cannot support the fix. Use a concrete failure case from the target branch's implementation and its callers.
- Identify the authoritative data, the derived result, and the exact operations that can change that result. Add invalidation hooks only for those operations. A component that displays or combines existing data does not automatically own its lifecycle.
- If a cache design requires hooks across unrelated subsystems, reconsider the cache design before expanding the patch. Do not use dependencies introduced by the proposed design as proof that the original bug requires that scope.
- Keep architectural changes in a separate proposal unless required for correctness. Before expanding a focused fix across unrelated subsystems, explain the dependency with a concrete example and the smallest viable alternative. Continue work already within scope.
- Tests must validate intended product behavior and realistic failures. Passing tests or tests that require every proposed hook do not establish that the design or expanded scope is necessary.
- For maintenance branches such as
1.2.x, prefer the smallest compatible fix. Avoid unrelated refactors, new dependencies, and expanded CI requirements.
- Before creating any pull request you MUST run
composer run-script php-cs-fixitto apply the repo'sphp-cs-fixerformatting rules, then commit any resulting changes. This script fixes in place (unlike the dry-runcomposer phpcsfixer), so creating a PR without running it is what trips the coding-standards job in CI. Run it from WSL (see Windows/WSL note below). Do not open the PR until the working tree is clean after the fixer runs. - Every commit must also pass PHPStan static analysis. Run
composer run-script phpstan -- --memory-limit=2Gfrom WSL and confirm it reportsNo errorsbefore opening the PR. The default 128M limit crashes a parallel worker, so always pass--memory-limit=2G(or higher). The repo config (.phpstan.neon) analyses at level 8, which is cumulative and therefore satisfies the Level 6 floor;tests/*is excluded from analysis, so test-only files will not surface here but production code (e.g.lib/) will.
- Windows/WSL: When working in VS Code from a Windows machine, run all fixes, builds, linters, tests, and git operations inside WSL (a Linux distro) rather than native Windows. The toolchain (PHP 8.3, Composer,
php-cs-fixer, Pest) and the repo's tab indentation /\nline-ending conventions are Linux-first; running them on native Windows produces spurious diffs and failures. Edit the WSL-mounted checkout (e.g. under/mnt/c/...or a native WSL path) and invokecomposerscripts such ascomposer php-cs-fixitfrom the WSL shell. - Install deps:
composer install(CI validates via.github/workflows/syntax.yml). - Install/upgrade DB:
php -q cli/install_cacti.php --accept-eula --install --forceandphp -q cli/upgrade_database.php --forcever=$(cat include/cacti_version). - Run poller:
php poller.php --poller=1 --force --debug(daemon debug:./cactid.php --foreground --debug). - Repo checks:
composer lint,composer phpstan,composer phpcsfixer(dry-run). - Refresh security baselines when the sink or hotspot guard reports drift:
tests/security/build_sink_inventory.sh | tr -d '\r' | LC_ALL=C sort > tests/security/baselines/sink_inventory.baseline.tsvand the matchingbuild_architectural_helper_report.sh --hotspotsforarchitectural_hotspots.baseline.tsv. Use the command the verifier prints rather than editing the file. The verifiers strip:LINEbefore comparing, so most of the resulting diff is renumbering they ignore; say in the pull request how many entries are substantive.
- Do not add, copy, or update third-party package source under
include/vendor/or elsewhere in the repository. Existing vendored dependencies may be removed by an in-scope change, but must not be expanded or refreshed. - Add PHP dependencies through Composer: update
composer.json, update and commitcomposer.lock, and letcomposer installinstall the locked packages in development, CI, and release builds. - Do not commit Composer-installed package trees or hand-edit Composer-generated autoload metadata. Validate dependency changes with
composer validateand a cleancomposer install. - If a third-party asset cannot be managed through Composer, stop and document the concrete packaging requirement before adding files. Do not silently create a vendored exception.
Treat polling performance as an architectural constraint when changing
poller.php, cmd.php, script_server.php, polling scripts, SNMP helpers,
subprocess wrappers, lib/poller.php or RRD update paths. Cacti installations
can have millions of data sources. Trace the actual call chain before adding
work: script-server launch, script include, function call, SNMP query, process
execution and individual SNMP requests. Preserve existing process reuse and
avoid introducing repeated includes, launches or network requests per sample.
- Resolve shared metadata at template scope and cache it once per process or explicitly defined polling cycle. Cache empty results with a separate loaded flag or equivalent sentinel; an empty array must not cause another query on every pass. Define invalidation for long-lived workers and configuration changes so the cache does not serve stale metadata indefinitely.
- Do not query or scan individual data sources inside every loop to answer a template-level question. Do not preload millions of source records as a substitute. Source-specific state, ownership, graph bindings, orphan handling and permissions still require correct source-specific checks; sharing a template does not make those values interchangeable. Batch necessary reads and keep cache cardinality and memory bounded.
- Do not add fixed sleeps to each subprocess or SNMP call. Prefer readiness waiting and monotonic deadlines where supported, while preserving active output handling, silent-child timeouts, exit status and resource cleanup. Measure overhead relative to direct execution. An extra 50 ms across 20,000 serialized calls adds approximately 1,000 seconds before useful work.
- Confirm the implementation on the target branch.
cacti_exec()and transport implementations differ across release branches; a fix or test for one branch must not be assumed applicable to another. - Treat credential caching as a separate optimization. Measure hit rates, avoided lookups, time and memory before claiming a benefit. Preserve device, poller and credential identity, invalidation and secret-handling boundaries; never log secrets as performance evidence. Credential caching does not fix repeated template queries or per-call process delays.
- For changes to these paths, run applicable polling performance regressions. Instrument query and call counts over repeated passes, including empty-cache results. Compare subprocess wrapper overhead with direct execution and cover streaming output, silent-child timeouts and exit codes. Exercise representative synthetic scales, including 150k, 1M and 2.5M sources where the branch's harness supports them, and record runtime, elapsed time and peak memory. Keep timings from hosted runners diagnostic; use reproducible count-based regressions and tests that detect reintroduced repeated queries or fixed delays.
- Label synthetic PHP processing results as synthetic. They do not establish real RTM throughput, database query plans, SNMP/Spine performance, RRD I/O, remote replication or full Boost behavior. Production-scale claims require a representative end-to-end benchmark, including those costs.
- Context: The full schema (DDL/DML) is in
cacti.sql. Check it for table structures and indexes. - Optimize: Proactively look for slow query patterns (e.g., missing indexes, non-sargable
WHEREclauses). - Best Practices:
- Prefer
JOINover subqueries where efficient. - Ensure columns in
WHERE/JOINclauses are indexed (checkcacti.sql). - Avoid
SELECT *in production code; list columns explicitly.
- Prefer
- Context: Cacti Takes Security Seriously consider the security implications when reviewing current and new code base
- Vulnerability Checks:
- SQL Injection: STRICTLY enforce prepared statements (
db_*_prepared). Flag any variable interpolation in SQL strings. - XSS: Ensure user input is sanitized/escaped before output (use
htmlspecialcharsor Cacti's wrappers). - CSRF: Verify forms/AJAX include
__csrf_magic. - Command Injection: Scrutinize
shell_exec,exec,passthru. Ensure arguments are escaped (escapeshellarg). - Auth: Verify
include/auth.phpis loaded and permissions are checked (api_user_realm_auth).
- SQL Injection: STRICTLY enforce prepared statements (
- Security advisories and CVE policy:
develop/1.3.xis unreleased. A vulnerability that exists only ondevelopdoes not get a CVE requested, since no released version is affected — document that determination in the GHSA description (a dated "Triage Note") instead.1.2.xis the released/LTS branch. If the same vulnerable code is also reachable on1.2.x(or was historically, for an unpatched released version), a CVE is requested.- Before deciding CVE vs. no-CVE, verify the claim against both branches' actual current source rather than trusting the reporter's stated affected-version range — it can be wrong (e.g. describing a bug already fixed on
1.2.xthat only regressed ondevelop). - If a
develop-only advisory is later found to also affect a released1.2.xversion, request a CVE at that point.
- While implementing a change, do small, behavior-preserving refactors in the touched area (dedupe logic, extract helpers, simplify conditions).
- Formatting: Enforce tabs for indentation (no spaces for indentation).
- Legacy: Avoid
preg_*if simple string functions work. Usepreg_*overereg_*. - Avoid drive-by rewrites: no repo-wide formatting, no unrelated cleanup, and preserve existing file conventions (keep the GPL header blocks).
-
Unit tests live in
tests/Unit/and use Pest (test()/it()functions, not PHPUnit classes). The Pest bootstrap istests/Pest.php; a lightweight source-scan bootstrap istests/bootstrap-unit.php. -
Tests are organized into category subfolders by domain. New tests must follow this structure — do not add flat files to
tests/Unit/root:Security/— security-related tests, subfoldered by vulnerability class (SqlInjection/,Xss/,Shell/,Auth/,Csrf/,Csp/,Redirect/,PathTraversal/,Ssrf/,Crypto/,InputValidation/,Session/,Ldap/,Type/,ShellInjection/,Misc/).DataCollection/— poller/SNMP/cmd/data-query tests (Poller/,Snmp/,Cmd/,DataQuery/,DataInput/).Ui/— front-end/graph rendering tests (Aggregate/,Graph/,Html/,Theme/,Colour/,Tree/,Spikekill/,Utility/).Core/— core library tests, with subfolders for distinct subsystems (Rrd/,RemoteAgent/,Automation/,Cli/,Mailer/,Boost/,Availability_Tests/).Database/,Installer/,Scripts/,Plugin/— domain-specific folders at the top level.
-
When adding a new test, place it in the matching category folder. If a new domain is needed, create a folder for it rather than leaving the file flat in
tests/Unit/. -
File naming: do not encode GitHub issue or GHSA numbers in filenames. Name the file after the behavior it tests (e.g.
PercentileContractTest.php, notIssue7070PercentileContractTest.php). The GHSA/issue ID belongs in the test'stest('...')description string or a file-level docblock, not the filename. -
__DIR__path depth: the depth depends on both the folder's nesting level and on what the path points at. Paths that reach the repository root need one more level than paths that stop attests/, so a single file often needs two different depths.test file location to reach repo root ( lib/,include/)to reach tests/(Helpers/,fixtures/,tools/)tests/Unit/dirname(__DIR__, 2)dirname(__DIR__)tests/Unit/<Category>/dirname(__DIR__, 3)dirname(__DIR__, 2)tests/Unit/<Category>/<Subcategory>/dirname(__DIR__, 4)dirname(__DIR__, 3)- After moving a test between folders, update every path in it, and check each one individually. Setting them all to the repo-root depth silently breaks the
tests/-internal ones. Arequirethat resolves to a missing file is fatal during bootstrap and stops the whole suite rather than failing one test. - Verify a move by running the suite, not by reading the diff.
- After moving a test between folders, update every path in it, and check each one individually. Setting them all to the repo-root depth silently breaks the
include/layout.js(select2-multi-countinit, both this branch andfeature/select2-select-conversion-1.2.x) works around select2 4.1.0 not natively supporting "search from the dropdown panel instead of an inline box" for multi-selects. It manually builds customdropdownAdapter/selectionAdaptercombinations via$.fn.select2.amd.require('select2/...')(synchronous string-form require - see the comment at that call site for why array-form must not be used).- TheWitness has an upstream pull request open against
select2/select2proposing this behavior as a first-class option:https://github.com/select2/select2/pull/6462 - Action item for any future Copilot session working in this repo: when
starting work here, check whether a select2 release has shipped that
includes that PR (check
select2/select2releases/CHANGELOG for a first-class dropdown-search-for-multiple-selects option). If it has:- Bump the vendored
include/js/select2.jsto that release. - Replace the manual adapter-composition workaround in
include/layout.jswith the new native option on bothfeature/select2-select-conversion(develop) andfeature/select2-select-conversion-1.2.x(1.2.x), or their successor branches/develop/1.2.x if already merged. - Remove the now-unnecessary
multiCountDropdownAdapter/multiCountSelectionAdapterwiring and related comments once the native option covers it. - Open a Cacti PR for this cleanup as a matter of course - don't wait to be asked.
- Bump the vendored