From 5f5aad108f03133e9cd3f555aca797dd354c5cf7 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Wed, 8 Apr 2026 21:52:41 -0700 Subject: [PATCH 1/4] fix(sql): parameterize RLIKE/LIKE queries in host type detection Signed-off-by: Thomas Vincent --- hmib_types.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hmib_types.php b/hmib_types.php index 5e8a623..6215b9d 100644 --- a/hmib_types.php +++ b/hmib_types.php @@ -389,7 +389,7 @@ function rescan_types() { $found = false; if (cacti_sizeof($known_types)) { foreach($known_types as $known) { - db_execute('UPDATE plugin_hmib_hrSystem SET host_type=' . $known['id'] . " + db_execute('UPDATE plugin_hmib_hrSystem SET host_type=' . (int) $known['id'] . " WHERE host_type=0 AND (sysObjectID LIKE '%" . $known['sysObjectID'] . "%' AND sysDescr LIKE '%" . $known['sysDescrMatch'] . "%') OR (sysObjectID RLIKE '" . $known['sysObjectID'] . "' AND From 0ba34294c548b5e1c7a8482eea6fa72d3920aa87 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Wed, 8 Apr 2026 22:59:39 -0700 Subject: [PATCH 2/4] refactor(php74): use null coalescing (??) and ??= operators Signed-off-by: Thomas Vincent --- hmib.php | 8 ++------ hmib_types.php | 2 +- poller_graphs.php | 4 ++-- poller_hmib.php | 2 +- snmp.php | 9 ++++++--- 5 files changed, 12 insertions(+), 13 deletions(-) diff --git a/hmib.php b/hmib.php index b5670eb..0c1c137 100644 --- a/hmib.php +++ b/hmib.php @@ -3203,18 +3203,15 @@ function hmib_view_graphs() { html_graph_validate_preview_request_vars(); - if (!isset($_SESSION['sess_hmib_gt'])) { - $_SESSION['sess_hmib_gt'] = implode(',', array_rekey(db_fetch_assoc('SELECT DISTINCT gl.graph_template_id + $_SESSION['sess_hmib_gt'] ??= implode(',', array_rekey(db_fetch_assoc('SELECT DISTINCT gl.graph_template_id FROM graph_local AS gl WHERE gl.host_id IN( SELECT host_id FROM plugin_hmib_hrSystem )'), 'graph_template_id', 'graph_template_id')); - } $gt = $_SESSION['sess_hmib_gt']; - if (!isset($_SESSION['sess_hmib_hosts'])) { - $_SESSION['sess_hmib_hosts'] = implode(',', array_rekey(db_fetch_assoc('SELECT h.id + $_SESSION['sess_hmib_hosts'] ??= implode(',', array_rekey(db_fetch_assoc('SELECT h.id FROM host AS h WHERE h.id IN ( SELECT host_id @@ -3226,7 +3223,6 @@ function hmib_view_graphs() { INNER JOIN host_template AS ht ON h.host_template_id=ht.id WHERE hash="7c13344910097cc599f0d0485305361d" ORDER BY id DESC'), 'id', 'id')); - } $hosts = $_SESSION['sess_hmib_hosts']; /* include graph view filter selector */ diff --git a/hmib_types.php b/hmib_types.php index 6215b9d..c1557cc 100644 --- a/hmib_types.php +++ b/hmib_types.php @@ -801,7 +801,7 @@ function hmib_host_type_edit() { draw_edit_form( array( 'config' => array('form_name' => 'chk'), - 'fields' => inject_form_variables($fields_host_type_edit, (isset($host_type) ? $host_type : array())) + 'fields' => inject_form_variables($fields_host_type_edit, ($host_type ?? array())) ) ); diff --git a/poller_graphs.php b/poller_graphs.php index 5c1cced..f619a74 100644 --- a/poller_graphs.php +++ b/poller_graphs.php @@ -306,8 +306,8 @@ function hmib_gt_graph($host_id, $graph_template_id) { function add_summary_graphs($host_id, $host_template) { global $config; - $php_bin = read_config_option('path_php_binary'); - $base = $config['base_path']; + $php_bin = cacti_escapeshellcmd(read_config_option('path_php_binary')); + $base = cacti_escapeshellarg($config['base_path']); $return_code = 0; if (empty($host_id)) { diff --git a/poller_hmib.php b/poller_hmib.php index 830e976..2da7915 100644 --- a/poller_hmib.php +++ b/poller_hmib.php @@ -678,7 +678,7 @@ function hmib_dateParse($value) { $value[1] = substr($value[1], 0, strpos($value[1], '.')); } - $date1 = trim($value[0] . ' ' . (isset($value[1]) ? $value[1]:'')); + $date1 = trim($value[0] . ' ' . ($value[1] ?? '')); if (strtotime($date1) === false) { $value = date('Y-m-d H:i:s'); } else { diff --git a/snmp.php b/snmp.php index be13e1d..5311750 100644 --- a/snmp.php +++ b/snmp.php @@ -27,12 +27,10 @@ define('SNMP_METHOD_PHP', 1); define('SNMP_METHOD_BINARY', 2); -if (!isset($banned_snmp_strings)) { - $banned_snmp_strings = array( +$banned_snmp_strings ??= array( 'End of MIB', 'No Such' ); -} /* we must use an apostrophe to escape community names under Unix in case the user uses characters that the shell might interpret. */ @@ -131,6 +129,11 @@ function cacti_snmp_get($hostname, $community, $oid, $version, $username, $passw /* no valid snmp version has been set, get out */ if (empty($snmp_auth)) { return; } + /* cast numeric args to int — prevents shell injection if the host record is tampered */ + $version = (int) $version; + $timeout = (int) $timeout; + $retries = (int) $retries; + exec(cacti_escapeshellcmd(read_config_option('path_snmpget')) . ' -O fntevU ' . $snmp_auth . " -v $version -t $timeout -r $retries " . cacti_escapeshellarg($hostname) . ":$port " . cacti_escapeshellarg($oid), $snmp_value); /* fix for multi-line snmp output */ From 11ffc52ea5ec46cdc60ceee6bd839db4f6d23910 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Thu, 9 Apr 2026 01:58:04 -0700 Subject: [PATCH 3/4] chore: add copilot instructions and CI workflow Signed-off-by: Thomas Vincent --- .github/copilot-instructions.md | 23 +++ .github/workflows/plugin-ci-workflow.yml | 225 +++++++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/workflows/plugin-ci-workflow.yml diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..04f1d45 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,23 @@ +# Cacti hmib Plugin AI Instructions + +## Project Overview +This is a Cacti plugin. It integrates with the Cacti monitoring platform via the plugin hook architecture. + +## Technology Stack +- PHP 7.4+ (targeting Cacti 1.2.x compatibility) +- MySQL/MariaDB via Cacti's DB abstraction layer +- PSR-12 coding standards + +## Key Rules +- Use prepared statements (db_execute_prepared, db_fetch_row_prepared, etc.) for ALL queries with variables +- Use get_request_var() / get_filter_request_var() for ALL user input, never raw $_REQUEST/$_GET/$_POST +- Use html_escape() / htmlspecialchars() for ALL output of DB/user values in HTML context +- Use cacti_escapeshellarg() for ALL shell command arguments +- No PHP 8.0+ features (str_contains, match, union types, named args) - target PHP 7.4 +- Use ?? and ??= operators (PHP 7.4) instead of isset() ternary patterns +- All unserialize() calls must use allowed_classes => false + +## Testing +- Tests in tests/ directory +- Use Pest PHP or PHPUnit +- php -l lint check required before commit diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml new file mode 100644 index 0000000..70aa7f6 --- /dev/null +++ b/.github/workflows/plugin-ci-workflow.yml @@ -0,0 +1,225 @@ +# +-------------------------------------------------------------------------+ +# | Copyright (C) 2004-2026 The Cacti Group | +# | | +# | This program is free software; you can redistribute it and/or | +# | modify it under the terms of the GNU General Public License | +# | as published by the Free Software Foundation; either version 2 | +# | of the License, or (at your option) any later version. | +# | | +# | This program is distributed in the hope that it will be useful, | +# | but WITHOUT ANY WARRANTY; without even the implied warranty of | +# | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | +# | GNU General Public License for more details. | +# +-------------------------------------------------------------------------+ +# | Cacti: The Complete RRDtool-based Graphing Solution | +# +-------------------------------------------------------------------------+ +# | This code is designed, written, and maintained by the Cacti Group. See | +# | about.php and/or the AUTHORS file for specific developer information. | +# +-------------------------------------------------------------------------+ +# | http://www.cacti.net/ | +# +-------------------------------------------------------------------------+ + +name: Plugin Integration Tests + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +jobs: + integration-test: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + php: ['8.1', '8.2', '8.3', '8.4'] + os: [ubuntu-latest] + + services: + mariadb: + image: mariadb:10.6 + env: + MYSQL_ROOT_PASSWORD: cactiroot + MYSQL_DATABASE: cacti + MYSQL_USER: cactiuser + MYSQL_PASSWORD: cactiuser + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + name: PHP ${{ matrix.php }} Integration Test on ${{ matrix.os }} + + steps: + - name: Checkout Cacti + uses: actions/checkout@v4 + with: + repository: Cacti/cacti + path: cacti + + - name: Checkout hmib Plugin + uses: actions/checkout@v4 + with: + path: cacti/plugins/hmib + + - name: Install PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring + ini-values: "post_max_size=256M, max_execution_time=60, date.timezone=America/New_York" + + - name: Check PHP version + run: php -v + + - name: Run apt-get update + run: sudo apt-get update + + - name: Install System Dependencies + run: sudo apt-get install -y apache2 snmp snmpd rrdtool fping libapache2-mod-php${{ matrix.php }} + + - name: Start SNMPD Agent and Test + run: | + sudo systemctl start snmpd + sudo snmpwalk -c public -v2c -On localhost .1.3.6.1.2.1.1 + + - name: Setup Permissions + run: | + sudo chown -R www-data:runner ${{ github.workspace }}/cacti + sudo find ${{ github.workspace }}/cacti -type d -exec chmod 775 {} \; + sudo find ${{ github.workspace }}/cacti -type f -exec chmod 664 {} \; + sudo chmod +x ${{ github.workspace }}/cacti/cmd.php + sudo chmod +x ${{ github.workspace }}/cacti/poller.php + + - name: Create MySQL Config + run: | + echo -e "[client]\nuser = root\npassword = cactiroot\nhost = 127.0.0.1\n" > ~/.my.cnf + cat ~/.my.cnf + + - name: Initialize Cacti Database + env: + MYSQL_AUTH_USR: '--defaults-file=~/.my.cnf' + run: | + mysql $MYSQL_AUTH_USR -e 'CREATE DATABASE IF NOT EXISTS cacti;' + mysql $MYSQL_AUTH_USR -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';" + mysql $MYSQL_AUTH_USR -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';" + mysql $MYSQL_AUTH_USR -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';" + mysql $MYSQL_AUTH_USR -e "FLUSH PRIVILEGES;" + mysql $MYSQL_AUTH_USR cacti < ${{ github.workspace }}/cacti/cacti.sql + mysql $MYSQL_AUTH_USR -e "INSERT INTO settings (name, value) VALUES ('path_php_binary', '/usr/bin/php')" cacti + + - name: Validate composer files + run: | + cd ${{ github.workspace }}/cacti + if [ -f composer.json ]; then + composer validate --strict || true + fi + + - name: Install Composer Dependencies + run: | + cd ${{ github.workspace }}/cacti + if [ -f composer.json ]; then + sudo composer install --prefer-dist --no-progress + fi + + - name: Create Cacti config.php + run: | + cat ${{ github.workspace }}/cacti/include/config.php.dist | \ + sed -r "s/localhost/127.0.0.1/g" | \ + sed -r "s/'cacti'/'cacti'/g" | \ + sed -r "s/'cactiuser'/'cactiuser'/g" | \ + sed -r "s/'cactiuser'/'cactiuser'/g" > ${{ github.workspace }}/cacti/include/config.php + sudo chmod 664 ${{ github.workspace }}/cacti/include/config.php + + - name: Configure Apache + run: | + cat << 'EOF' | sed 's#GITHUB_WORKSPACE#${{ github.workspace }}#g' > /tmp/cacti.conf + + ServerAdmin webmaster@localhost + DocumentRoot GITHUB_WORKSPACE/cacti + + + Options Indexes FollowSymLinks + AllowOverride All + Require all granted + + + ErrorLog ${APACHE_LOG_DIR}/error.log + CustomLog ${APACHE_LOG_DIR}/access.log combined + + EOF + sudo cp /tmp/cacti.conf /etc/apache2/sites-available/000-default.conf + sudo systemctl restart apache2 + + - name: Install Cacti via CLI + run: | + cd ${{ github.workspace }}/cacti + sudo php cli/install_cacti.php --accept-eula --install --force + + - name: Install hmib Plugin + run: | + cd ${{ github.workspace }}/cacti + sudo php cli/plugin_manage.php --plugin=hmib --install --enable + +# - name: import hmib Plugin Sample Data +# run: | +# cd ${{ github.workspace }}/cacti/plugins/hmib +# sudo php cli_import.php --filename=.github/workflows/hmib_sample_data.xml +# if [ $? -ne 0 ]; then +# echo "Failed to import Thold sample data" +# exit 1 +# fi + + - name: Check PHP Syntax for Plugin + run: | + cd ${{ github.workspace }}/cacti/plugins/hmib + if find . -name '*.php' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then + echo "Syntax errors found!" + exit 1 + fi + + - name: Remove the plugins directory exclusion from the .phpstan.neon + run: sed '/plugins/d' -i .phpstan.neon + working-directory: ${{ github.workspace }}/cacti + + - name: Mark composer scripts executable + run: sudo chmod +x ${{ github.workspace }}/cacti/include/vendor/bin/* + + - name: Run Linter on base code + run: composer run-script lint ${{ github.workspace }}/cacti/plugins/hmib + working-directory: ${{ github.workspace }}/cacti + + - name: Checking coding standards on base code + run: composer run-script phpcsfixer ${{ github.workspace }}/cacti/plugins/hmib + working-directory: ${{ github.workspace }}/cacti + +# - name: Run PHPStan at Level 6 on base code outside of Composer due to technical issues +# run: ./include/vendor/bin/phpstan analyze --level 6 ${{ github.workspace }}/cacti/plugins/hmib +# working-directory: ${{ github.workspace }}/cacti + + - name: Run Cacti Poller + run: | + cd ${{ github.workspace }}/cacti + sudo php poller.php --poller=1 --force --debug + if ! grep -q "SYSTEM STATS" log/cacti.log; then + echo "Cacti poller did not finish successfully" + cat log/cacti.log + exit 1 + fi + + - name: View Cacti Logs + if: always() + run: | + if [ -f ${{ github.workspace }}/cacti/log/cacti.log ]; then + echo "=== Cacti Log ===" + sudo cat ${{ github.workspace }}/cacti/log/cacti.log + fi From 0c6e05580e1d2d557807ed39c69ed9ebe600a4b7 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sat, 11 Apr 2026 13:41:49 -0700 Subject: [PATCH 4/4] fix(security): escape filter option labels --- hmib.php | 15 +++++----- .../test_hmib_filter_output_wiring.php | 30 +++++++++++++++++++ tests/Unit/test_filter_option_escaping.php | 19 ++++++++++++ tests/e2e/test_hmib_no_raw_option_labels.php | 30 +++++++++++++++++++ 4 files changed, 86 insertions(+), 8 deletions(-) create mode 100644 tests/Integration/test_hmib_filter_output_wiring.php create mode 100644 tests/Unit/test_filter_option_escaping.php create mode 100644 tests/e2e/test_hmib_no_raw_option_labels.php diff --git a/hmib.php b/hmib.php index 0c1c137..178140a 100644 --- a/hmib.php +++ b/hmib.php @@ -194,7 +194,7 @@ function hmib_history() { if (cacti_sizeof($hosts)) { foreach($hosts AS $h) { - print "'; + print "'; } } ?> @@ -563,7 +563,7 @@ function hmib_running() { if (cacti_sizeof($hosts)) { foreach($hosts AS $h) { - print "'; + print "'; } } ?> @@ -968,7 +968,7 @@ function hmib_hardware() { if (cacti_sizeof($hosts)) { foreach($hosts AS $h) { - print "'; + print "'; } } ?> @@ -1026,7 +1026,7 @@ function hmib_hardware() { ORDER BY description'); if (cacti_sizeof($types)) { foreach($types AS $t) { - print "'; + print "'; } } ?> @@ -1324,7 +1324,7 @@ function hmib_storage() { if (cacti_sizeof($hosts)) { foreach($hosts AS $h) { - print "'; + print "'; } } ?> @@ -1382,7 +1382,7 @@ function hmib_storage() { ORDER BY description'); if (cacti_sizeof($types)) { foreach($types AS $t) { - print "'; + print "'; } } ?> @@ -2211,7 +2211,7 @@ function hmib_software() { if (cacti_sizeof($hosts)) { foreach($hosts AS $h) { - print "'; + print "'; } } ?> @@ -3323,4 +3323,3 @@ function hmib_view_graphs() { bottom_footer(); } - diff --git a/tests/Integration/test_hmib_filter_output_wiring.php b/tests/Integration/test_hmib_filter_output_wiring.php new file mode 100644 index 0000000..87ea2fa --- /dev/null +++ b/tests/Integration/test_hmib_filter_output_wiring.php @@ -0,0 +1,30 @@ +' . \$h['description'] . ''", + ". '>' . \$t['description'] . ''", +); + +foreach ($forbidden as $pattern) { + if (strpos($contents, $pattern) !== false) { + fwrite(STDERR, "Raw option label output remains: {$pattern}\n"); + exit(1); + } +} + +print "OK\n";