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..07ec6b0 --- /dev/null +++ b/.github/workflows/plugin-ci-workflow.yml @@ -0,0 +1,228 @@ +# +-------------------------------------------------------------------------+ +# | 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 + +permissions: + contents: read + +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@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + repository: Cacti/cacti + path: cacti + + - name: Checkout hmib Plugin + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + path: cacti/plugins/hmib + + - name: Install PHP ${{ matrix.php }} + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 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 + + - 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 diff --git a/.gitignore b/.gitignore index 6621d40..f857b7e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ .git* locales/po/*.mo +.omc/ diff --git a/hmib.php b/hmib.php index b5670eb..48676a8 100644 --- a/hmib.php +++ b/hmib.php @@ -1,4 +1,6 @@ array( + 'rows' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1' - ), - 'page' => array( + ], + 'page' => [ 'filter' => FILTER_VALIDATE_INT, 'default' => '1' - ), - 'template' => array( + ], + 'template' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'device' => array( + ], + 'device' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'ostype' => array( + ], + 'ostype' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), + ], 'process' => array( 'filter' => FILTER_CALLBACK, 'pageset' => true, 'default' => '-1', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), - 'filter' => array( + 'filter' => [ 'filter' => FILTER_DEFAULT, 'pageset' => true, 'default' => '' - ), + ], 'sort_column' => array( 'filter' => FILTER_CALLBACK, 'default' => 'name', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_direction' => array( 'filter' => FILTER_CALLBACK, 'default' => 'ASC', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ) ); @@ -315,7 +317,7 @@ function clearFilter() { } $sql_where = "WHERE hrswls.name!='' AND hrswls.name!='System Idle Process'"; - $sql_params = array(); + $sql_params = []; $sql_limit = ' LIMIT ' . ($num_rows*(get_request_var('page')-1)) . ',' . $num_rows; $sql_order = get_order_string(); @@ -466,51 +468,51 @@ function hmib_running() { /* ================= input validation and session storage ================= */ $filters = array( - 'rows' => array( + 'rows' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1' - ), - 'page' => array( + ], + 'page' => [ 'filter' => FILTER_VALIDATE_INT, 'default' => '1' - ), - 'template' => array( + ], + 'template' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'device' => array( + ], + 'device' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'ostype' => array( + ], + 'ostype' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), + ], 'filter' => array( 'filter' => FILTER_CALLBACK, 'pageset' => true, 'default' => '', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'process' => array( 'filter' => FILTER_CALLBACK, 'pageset' => true, 'default' => '-1', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_column' => array( 'filter' => FILTER_CALLBACK, 'default' => 'name', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_direction' => array( 'filter' => FILTER_CALLBACK, 'default' => 'ASC', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ) ); @@ -685,7 +687,7 @@ function clearFilter() { $sql_limit = ' LIMIT ' . ($num_rows*(get_request_var('page')-1)) . ',' . $num_rows; $sql_where = "WHERE hrswr.name != '' AND hrswr.name != 'System Idle Process'"; - $sql_params = array(); + $sql_params = []; $sql_order = get_order_string(); if (get_request_var('template') != '-1') { @@ -866,56 +868,56 @@ function hmib_hardware() { /* ================= input validation and session storage ================= */ $filters = array( - 'rows' => array( + 'rows' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1' - ), - 'page' => array( + ], + 'page' => [ 'filter' => FILTER_VALIDATE_INT, 'default' => '1' - ), - 'template' => array( + ], + 'template' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'device' => array( + ], + 'device' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'type' => array( + ], + 'type' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'ostype' => array( + ], + 'ostype' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), + ], 'process' => array( 'filter' => FILTER_CALLBACK, 'pageset' => true, 'default' => '-1', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'filter' => array( 'filter' => FILTER_CALLBACK, 'pageset' => true, 'default' => '', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_column' => array( 'filter' => FILTER_CALLBACK, 'default' => 'hrd.description', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_direction' => array( 'filter' => FILTER_CALLBACK, 'default' => 'ASC', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ) ); @@ -1088,7 +1090,7 @@ function clearFilter() { } $sql_where = "WHERE (hrd.description IS NOT NULL AND hrd.description!='')"; - $sql_params = array(); + $sql_params = []; $sql_limit = ' LIMIT ' . ($num_rows*(get_request_var('page')-1)) . ',' . $num_rows; $sql_order = get_order_string(); @@ -1219,56 +1221,56 @@ function hmib_storage() { /* ================= input validation and session storage ================= */ $filters = array( - 'rows' => array( + 'rows' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1' - ), - 'page' => array( + ], + 'page' => [ 'filter' => FILTER_VALIDATE_INT, 'default' => '1' - ), - 'template' => array( + ], + 'template' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'device' => array( + ], + 'device' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'type' => array( + ], + 'type' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'ostype' => array( + ], + 'ostype' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), + ], 'process' => array( 'filter' => FILTER_CALLBACK, 'pageset' => true, 'default' => '-1', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'filter' => array( 'filter' => FILTER_CALLBACK, 'pageset' => true, 'default' => '', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_column' => array( 'filter' => FILTER_CALLBACK, 'default' => 'hrsto.description', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_direction' => array( 'filter' => FILTER_CALLBACK, 'default' => 'ASC', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ) ); @@ -1444,7 +1446,7 @@ function clearFilter() { } $sql_where = "WHERE (hrsto.description IS NOT NULL AND hrsto.description!='')"; - $sql_params = array(); + $sql_params = []; $sql_limit = ' LIMIT ' . ($num_rows*(get_request_var('page')-1)) . ',' . $num_rows; $sql_order = get_order_string(); @@ -1593,51 +1595,51 @@ function hmib_devices() { /* ================= input validation and session storage ================= */ $filters = array( - 'rows' => array( + 'rows' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1' - ), - 'page' => array( + ], + 'page' => [ 'filter' => FILTER_VALIDATE_INT, 'default' => '1' - ), - 'template' => array( + ], + 'template' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), + ], 'process' => array( 'filter' => FILTER_CALLBACK, - 'options' => array('options' => 'sanitize_search_string'), + 'options' => ['options' => 'sanitize_search_string'], 'pageset' => true, 'default' => '-1', ), - 'status' => array( + 'status' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'ostype' => array( + ], + 'ostype' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), + ], 'filter' => array( 'filter' => FILTER_CALLBACK, 'pageset' => true, 'default' => '', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_column' => array( 'filter' => FILTER_CALLBACK, 'default' => 'description', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_direction' => array( 'filter' => FILTER_CALLBACK, 'default' => 'ASC', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ) ); @@ -1745,7 +1747,7 @@ function hmib_devices() { INNER JOIN plugin_hmib_hrSystem ON host.id=plugin_hmib_hrSystem.host_id'); - $statuses = array_merge($statuses, array('-2' => array('status' => '-2'))); + $statuses = array_merge($statuses, array('-2' => ['status' => '-2'])); if (cacti_sizeof($statuses)) { foreach($statuses AS $s) { @@ -1829,7 +1831,7 @@ function clearFilter() { $sql_limit = ' LIMIT ' . ($num_rows*(get_request_var('page')-1)) . ',' . $num_rows; $sql_where = ''; - $sql_params = array(); + $sql_params = []; $sql_order = get_order_string(); if (get_request_var('template') != '-1') { @@ -2112,50 +2114,50 @@ function hmib_software() { /* ================= input validation and session storage ================= */ $filters = array( - 'rows' => array( + 'rows' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1' - ), - 'page' => array( + ], + 'page' => [ 'filter' => FILTER_VALIDATE_INT, 'default' => '1' - ), - 'template' => array( + ], + 'template' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'device' => array( + ], + 'device' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'type' => array( + ], + 'type' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), - 'ostype' => array( + ], + 'ostype' => [ 'filter' => FILTER_VALIDATE_INT, 'pageset' => true, 'default' => '-1', - ), + ], 'filter' => array( 'filter' => FILTER_CALLBACK, 'pageset' => true, 'default' => '', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_column' => array( 'filter' => FILTER_CALLBACK, 'default' => 'name', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_direction' => array( 'filter' => FILTER_CALLBACK, 'default' => 'ASC', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ) ); @@ -2332,7 +2334,7 @@ function clearFilter() { $sql_limit = ' LIMIT ' . ($num_rows*(get_request_var('page')-1)) . ',' . $num_rows; $sql_where = ''; - $sql_params = array(); + $sql_params = []; $sql_order = get_order_string(); if (get_request_var('template') != '-1') { @@ -2506,24 +2508,24 @@ function hmib_summary() { 'pageset' => true, 'default' => read_config_option('hmib_top_processes') ), - 'page' => array( + 'page' => [ 'filter' => FILTER_VALIDATE_INT, 'default' => '1' - ), - 'filter' => array( + ], + 'filter' => [ 'filter' => FILTER_DEFAULT, 'pageset' => true, 'default' => '' - ), + ], 'sort_column' => array( 'filter' => FILTER_CALLBACK, 'default' => 'maxCpu', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_direction' => array( 'filter' => FILTER_CALLBACK, 'default' => 'DESC', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ) ); @@ -2569,12 +2571,12 @@ function hmib_summary() { 'sort_column' => array( 'filter' => FILTER_CALLBACK, 'default' => 'upHosts', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ), 'sort_direction' => array( 'filter' => FILTER_CALLBACK, 'default' => 'DESC', - 'options' => array('options' => 'sanitize_search_string') + 'options' => ['options' => 'sanitize_search_string'] ) ); @@ -2888,7 +2890,7 @@ function clearHostFilter() {