From 4c325288f4ca1064f2ab24161b2fd1f06ad37b37 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 12 Jul 2026 13:54:45 -0700 Subject: [PATCH 01/16] ci: modernize Monitor workflow --- .github/workflows/plugin-ci-workflow.yml | 73 +++++++++++++++--------- 1 file changed, 45 insertions(+), 28 deletions(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 1b63343..aa9472e 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -19,17 +19,35 @@ # | http://www.cacti.net/ | # +-------------------------------------------------------------------------+ -name: Plugin Integration Tests +name: Monitor CI on: push: branches: - main - develop + paths: + - '**.php' + - '.github/workflows/plugin-ci-workflow.yml' + - 'composer.json' + - 'composer.lock' pull_request: branches: - main - develop + paths: + - '**.php' + - '.github/workflows/plugin-ci-workflow.yml' + - 'composer.json' + - 'composer.lock' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: monitor-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: integration-test: @@ -38,12 +56,12 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.1', '8.2', '8.3', '8.4'] + php: ['7.4', '8.1', '8.4'] os: [ubuntu-latest] services: mariadb: - image: mariadb:10.6 + image: mariadb:10.11 env: MYSQL_ROOT_PASSWORD: cactiroot MYSQL_DATABASE: cacti @@ -81,11 +99,10 @@ jobs: - 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 + run: | + sudo apt-get update + sudo apt-get install --yes --no-install-recommends apache2 snmp snmpd rrdtool fping libapache2-mod-php - name: Start SNMPD Agent and Test run: | @@ -95,40 +112,35 @@ jobs: - 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 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' + MYSQL_PWD: cactiroot 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 + mysql --host=127.0.0.1 --user=root -e 'CREATE DATABASE IF NOT EXISTS cacti;' + mysql --host=127.0.0.1 --user=root -e "CREATE USER IF NOT EXISTS 'cactiuser'@'localhost' IDENTIFIED BY 'cactiuser';" + mysql --host=127.0.0.1 --user=root -e "GRANT ALL PRIVILEGES ON cacti.* TO 'cactiuser'@'localhost';" + mysql --host=127.0.0.1 --user=root -e "GRANT SELECT ON mysql.time_zone_name TO 'cactiuser'@'localhost';" + mysql --host=127.0.0.1 --user=root -e "FLUSH PRIVILEGES;" + mysql --host=127.0.0.1 --user=root cacti < ${{ github.workspace }}/cacti/cacti.sql + mysql --host=127.0.0.1 --user=root -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 + composer validate --strict fi - name: Install Composer Dependencies run: | cd ${{ github.workspace }}/cacti if [ -f composer.json ]; then - sudo composer install --prefer-dist --no-progress + composer install --prefer-dist --no-interaction --no-progress fi - name: Create Cacti config.php @@ -173,10 +185,7 @@ jobs: - name: Check PHP Syntax for Plugin run: | cd ${{ github.workspace }}/cacti/plugins/monitor - if find . -name '*.php' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then - echo "Syntax errors found!" - exit 1 - fi + find . -path './vendor' -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l - name: Remove the plugins directory exclusion from the .phpstan.neon run: sed '/plugins/d' -i .phpstan.neon @@ -214,3 +223,11 @@ jobs: echo "=== Cacti Log ===" sudo cat ${{ github.workspace }}/cacti/log/cacti.log fi + + - name: Upload Cacti log + if: failure() + uses: actions/upload-artifact@v4 + with: + name: cacti-log-php-${{ matrix.php }} + path: ${{ github.workspace }}/cacti/log/cacti.log + if-no-files-found: ignore From a06ae88543e0c7b51f01fada93339a288c3625bc Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 12 Jul 2026 22:58:45 -0700 Subject: [PATCH 02/16] ci: pin integration tests to Cacti 1.2.31 --- .github/workflows/plugin-ci-workflow.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index aa9472e..407d802 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -82,6 +82,7 @@ jobs: uses: actions/checkout@v5 with: repository: Cacti/cacti + ref: release/1.2.31 path: cacti - name: Checkout Monitor Plugin From 0547614e73f3a9fc541a86872a865d2ec82b7911 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 13 Jul 2026 01:56:46 -0700 Subject: [PATCH 03/16] ci(security): pin GitHub Actions to commit SHAs Pin actions/checkout (v5), shivammathur/setup-php (v2) and actions/upload-artifact (v4) to full commit SHAs with a trailing # vX comment. Mutable tags let a compromised or force-pushed tag run arbitrary code in CI; pinning to an immutable SHA is the supply-chain-hardening best practice and matches the SHA-pinned checkout used in the core security workflow. --- .github/workflows/plugin-ci-workflow.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 407d802..90bc40d 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -79,19 +79,19 @@ jobs: steps: - name: Checkout Cacti - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: repository: Cacti/cacti ref: release/1.2.31 path: cacti - name: Checkout Monitor Plugin - uses: actions/checkout@v5 + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 with: path: cacti/plugins/monitor - name: Install PHP ${{ matrix.php }} - uses: shivammathur/setup-php@v2 + uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 with: php-version: ${{ matrix.php }} extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring @@ -227,7 +227,7 @@ jobs: - name: Upload Cacti log if: failure() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: cacti-log-php-${{ matrix.php }} path: ${{ github.workspace }}/cacti/log/cacti.log From 3f5e186fd3e0f758ed4e94485beb1aa592172baf Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 13 Jul 2026 23:01:02 -0700 Subject: [PATCH 04/16] Allow Cacti dependency validation warnings --- .github/workflows/plugin-ci-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 90bc40d..5cccb6d 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -134,7 +134,7 @@ jobs: run: | cd ${{ github.workspace }}/cacti if [ -f composer.json ]; then - composer validate --strict + composer validate --no-check-publish fi - name: Install Composer Dependencies From ce0698b5164396eb2554d723b17f79a6fcaddeae Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 13 Jul 2026 23:03:55 -0700 Subject: [PATCH 05/16] Align Monitor matrix with Cacti dependencies --- .github/workflows/plugin-ci-workflow.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 5cccb6d..563792e 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -56,7 +56,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['7.4', '8.1', '8.4'] + php: ['8.1', '8.4'] os: [ubuntu-latest] services: @@ -141,7 +141,7 @@ jobs: run: | cd ${{ github.workspace }}/cacti if [ -f composer.json ]; then - composer install --prefer-dist --no-interaction --no-progress + sudo composer install --prefer-dist --no-interaction --no-progress fi - name: Create Cacti config.php From a9ef8b0fd21953841fc246a7a30aa47fcf2f266b Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 13 Jul 2026 23:09:34 -0700 Subject: [PATCH 06/16] Fix Monitor PHPStan working directory --- .github/workflows/plugin-ci-workflow.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 563792e..6aa058e 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -191,6 +191,7 @@ jobs: - name: Remove the plugins directory exclusion from the .phpstan.neon run: sed '/plugins/d' -i .phpstan.neon working-directory: ${{ github.workspace }}/cacti + working-directory: ${{ github.workspace }}/cacti - name: Mark composer scripts executable run: sudo chmod +x ${{ github.workspace }}/cacti/include/vendor/bin/* From 8f5769bdde32484b49934e430e9c5375ce2215d7 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 13 Jul 2026 23:34:45 -0700 Subject: [PATCH 07/16] Fix duplicate Monitor workflow key --- .github/workflows/plugin-ci-workflow.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 6aa058e..563792e 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -191,7 +191,6 @@ jobs: - name: Remove the plugins directory exclusion from the .phpstan.neon run: sed '/plugins/d' -i .phpstan.neon working-directory: ${{ github.workspace }}/cacti - working-directory: ${{ github.workspace }}/cacti - name: Mark composer scripts executable run: sudo chmod +x ${{ github.workspace }}/cacti/include/vendor/bin/* From cca839602f5bd7da655cf6ef7b94ecef36f6b283 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 13 Jul 2026 23:44:34 -0700 Subject: [PATCH 08/16] Guard optional PHPStan configuration --- .github/workflows/plugin-ci-workflow.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 563792e..dfefa20 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -189,7 +189,10 @@ jobs: find . -path './vendor' -prune -o -name '*.php' -print0 | xargs -0 -n1 php -l - name: Remove the plugins directory exclusion from the .phpstan.neon - run: sed '/plugins/d' -i .phpstan.neon + run: | + if [ -f .phpstan.neon ]; then + sed '/plugins/d' -i .phpstan.neon + fi working-directory: ${{ github.workspace }}/cacti - name: Mark composer scripts executable From eea95f64df8d142a7da4993136eec31dee63dd89 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 13 Jul 2026 23:46:47 -0700 Subject: [PATCH 09/16] Guard optional Composer vendor binaries --- .github/workflows/plugin-ci-workflow.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index dfefa20..e05477c 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -196,7 +196,10 @@ jobs: working-directory: ${{ github.workspace }}/cacti - name: Mark composer scripts executable - run: sudo chmod +x ${{ github.workspace }}/cacti/include/vendor/bin/* + run: | + if [ -d "${{ github.workspace }}/cacti/include/vendor/bin" ]; then + sudo find "${{ github.workspace }}/cacti/include/vendor/bin" -maxdepth 1 -type f -exec chmod +x {} + + fi - name: Run Linter on base code run: composer run-script lint ${{ github.workspace }}/cacti/plugins/monitor From 219571c49aae3085e5d0578e9f6253e6c566ecfd Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 13 Jul 2026 23:49:34 -0700 Subject: [PATCH 10/16] Skip unavailable Composer quality scripts --- .github/workflows/plugin-ci-workflow.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index e05477c..0ecfe85 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -202,11 +202,21 @@ jobs: fi - name: Run Linter on base code - run: composer run-script lint ${{ github.workspace }}/cacti/plugins/monitor + run: | + if composer run-script --list | grep -qE '^ lint'; then + composer run-script lint ${{ github.workspace }}/cacti/plugins/monitor + else + echo 'Composer lint script is not defined; skipping.' + fi working-directory: ${{ github.workspace }}/cacti - name: Checking coding standards on base code - run: composer run-script phpcsfixer ${{ github.workspace }}/cacti/plugins/monitor + run: | + if composer run-script --list | grep -qE '^ phpcsfixer'; then + composer run-script phpcsfixer ${{ github.workspace }}/cacti/plugins/monitor + else + echo 'Composer phpcsfixer script is not defined; skipping.' + fi working-directory: ${{ github.workspace }}/cacti # - name: Run PHPStan at Level 6 on base code outside of Composer due to technical issues From 8e38688f573b2dad96b6f8052362b010fcf5d0c0 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Tue, 14 Jul 2026 03:31:35 -0700 Subject: [PATCH 11/16] ci: restore full PHP 8.1-8.4 integration matrix Keep 1.2.31 pin while covering mid-stream PHP releases. Signed-off-by: Thomas Vincent --- .github/workflows/plugin-ci-workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 0ecfe85..b09000d 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -56,7 +56,7 @@ jobs: strategy: fail-fast: false matrix: - php: ['8.1', '8.4'] + php: ['8.1', '8.2', '8.3', '8.4'] os: [ubuntu-latest] services: From d5c293dab909ab7dc86c6e5ccbd9ae80bb9e454e Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Sun, 16 Aug 2026 20:50:35 -0700 Subject: [PATCH 12/16] hardening: run the test suite, and add CodeQL The plugin shipped three test files that nothing ran: the workflow had no reference to tests/ at all, so a suite covering request-variable output escaping sat unexecuted. Adopts the Pest-through-composer pattern from thold and mactrack, with a bootstrap that stubs Cacti so plugin code loads without a full install. Two of the three existing files were meaningful grep guards against raw request reuse and are kept as Pest datasets; the third only asserted that htmlspecialchars() works. Pest 2 rather than mactrack's Pest 1, which no longer runs on current PHP. The PHP 8.0 runtime floor is enforced by the syntax-floor job rather than a composer platform pin, since the dev tooling needs 8.1. Stacked on #225, so the SHA pinning and permissions block are inherited rather than duplicated. Dependabot is left to #226. Signed-off-by: Thomas Vincent --- .github/workflows/codeql.yml | 59 ++++++++++ .github/workflows/plugin-ci-workflow.yml | 68 +++++++++++- .gitignore | 5 + composer.json | 21 ++++ phpunit.xml.dist | 11 ++ tests/Integration/OutputEscapingTest.php | 44 ++++++++ .../test_monitor_request_output_wiring.php | 41 ------- tests/Pest.php | 14 +++ tests/bootstrap.php | 101 ++++++++++++++++++ .../e2e/test_monitor_no_raw_request_reuse.php | 40 ------- tests/unit/DbFunctionsTest.php | 59 ++++++++++ tests/unit/test_request_output_escaping.php | 19 ---- 12 files changed, 379 insertions(+), 103 deletions(-) create mode 100644 .github/workflows/codeql.yml create mode 100644 composer.json create mode 100644 phpunit.xml.dist create mode 100644 tests/Integration/OutputEscapingTest.php delete mode 100644 tests/Integration/test_monitor_request_output_wiring.php create mode 100644 tests/Pest.php create mode 100644 tests/bootstrap.php delete mode 100644 tests/e2e/test_monitor_no_raw_request_reuse.php create mode 100644 tests/unit/DbFunctionsTest.php delete mode 100644 tests/unit/test_request_output_escaping.php diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..9dd80d5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,59 @@ +# +-------------------------------------------------------------------------+ +# | Copyright (C) 2004-2026 The Cacti Group | +# +-------------------------------------------------------------------------+ +# | Cacti: The Complete RRDtool-based Graphing Solution | +# +-------------------------------------------------------------------------+ +# | http://www.cacti.net/ | +# +-------------------------------------------------------------------------+ +# +# CodeQL has no PHP analysis, so this covers the plugin JavaScript only. The PHP is +# covered by the syntax and integration jobs in plugin-ci-workflow.yml. + +name: "CodeQL" + +on: + push: + branches: [main, develop] + paths-ignore: + - "**/*.md" + pull_request: + branches: [main, develop] + paths-ignore: + - "**/*.md" + schedule: + - cron: "30 1 * * 1" + workflow_dispatch: + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: ["javascript-typescript"] + steps: + - name: Checkout repository + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - name: Initialize CodeQL + uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + with: + languages: ${{ matrix.language }} + - name: Autobuild + uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index b09000d..ed587fd 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -31,6 +31,8 @@ on: - '.github/workflows/plugin-ci-workflow.yml' - 'composer.json' - 'composer.lock' + - 'phpunit.xml.dist' + - 'tests/**' pull_request: branches: - main @@ -40,6 +42,8 @@ on: - '.github/workflows/plugin-ci-workflow.yml' - 'composer.json' - 'composer.lock' + - 'phpunit.xml.dist' + - 'tests/**' workflow_dispatch: permissions: @@ -50,6 +54,64 @@ concurrency: cancel-in-progress: true jobs: + # The plugin ships tests that nothing ran: three files under tests/ that pass + # from a plain PHP CLI. They cover request-variable output escaping, which is + # the class of defect most likely to regress here. + # A duplicate key or schema error stops GitHub loading the file, which + # presents as a workflow that silently never runs. Catch it here instead. + workflow-lint: + name: Workflow syntax + runs-on: ubuntu-latest + steps: + - name: Checkout Monitor Plugin + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + + - name: Run actionlint + uses: docker://rhysd/actionlint@sha256:887a259a5a534f3c4f36cb02dca341673c6089431057242cdc931e9f133147e9 + with: + args: -color + + unit-test: + name: Standalone test suite + runs-on: ubuntu-latest + steps: + - name: Checkout monitor Plugin + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + + - name: Install PHP + uses: shivammathur/setup-php@b604ade2a87db23f8871b7182e69ec5e75effb45 # v2 + with: + php-version: '8.1' + tools: composer + + - name: Install dev dependencies + run: composer install --prefer-dist --no-progress --no-interaction --no-plugins --no-scripts + + - name: Run the test suite + run: composer test + + # compat = 1.2.15 implies Cacti 1.2.x, which requires PHP 8.0. The matrix + # below starts at 8.1, so it cannot catch syntax the declared floor rejects. + syntax-floor: + name: PHP 8.0 syntax floor + runs-on: ubuntu-latest + steps: + - name: Checkout monitor Plugin + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + + - name: Install PHP 8.0 + uses: shivammathur/setup-php@b604ade2a87db23f8871b7182e69ec5e75effb45 # v2 + with: + php-version: '8.0' + + - name: Lint every PHP file + run: | + if find . -path ./vendor -prune -o -name '*.php' -print0 \ + | xargs -0 -n1 php -l 2>&1 | grep -iv 'no syntax errors detected'; then + echo "Syntax errors found at the declared PHP floor" + exit 1 + fi + integration-test: runs-on: ${{ matrix.os }} @@ -79,19 +141,19 @@ jobs: steps: - name: Checkout Cacti - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 with: repository: Cacti/cacti ref: release/1.2.31 path: cacti - name: Checkout Monitor Plugin - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 with: path: cacti/plugins/monitor - name: Install PHP ${{ matrix.php }} - uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2 + uses: shivammathur/setup-php@b604ade2a87db23f8871b7182e69ec5e75effb45 # v2 with: php-version: ${{ matrix.php }} extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring diff --git a/.gitignore b/.gitignore index 7a6c551..6b564ca 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,8 @@ locales/po/*.mo vendor/ .omc/ + +# Lockfile is not committed: the plugin declares a range, and thold does the same. +composer.lock +/vendor/ +.phpunit.result.cache diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..8d5f995 --- /dev/null +++ b/composer.json @@ -0,0 +1,21 @@ +{ + "name": "cacti/plugin_monitor", + "description": "Monitor Plugin for Cacti", + "type": "cacti-plugin", + "license": "GPL-2.0-or-later", + "require": { + "php": ">=8.0" + }, + "require-dev": { + "pestphp/pest": "^2" + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } + }, + "scripts": { + "test": "pest", + "test:integration": "pest tests/Integration" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..72df35b --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,11 @@ + + + + + ./tests/unit + + + ./tests/Integration + + + diff --git a/tests/Integration/OutputEscapingTest.php b/tests/Integration/OutputEscapingTest.php new file mode 100644 index 0000000..05f258b --- /dev/null +++ b/tests/Integration/OutputEscapingTest.php @@ -0,0 +1,44 @@ +toContain($pattern); +})->with('escaped_outputs'); + +dataset('raw_reuse', [ + ['monitor_controller.php', "get_request_var('tree') . '\">'"], + ['monitor_controller.php', "get_request_var('site') . '\">'"], + ['monitor_controller.php', "get_request_var('template') . '\">'"], + ['monitor_controller.php', "get_request_var('size') . '\">'"], + ['monitor_controller.php', "get_request_var('trim') . '\">'"], + ['monitor_render.php', "monitor.php?rfilter=' . get_request_var('rfilter')"], +]); + +it('never concatenates a raw request value into markup', function (string $file, string $pattern) use ($root) { + expect(file_get_contents($root . '/' . $file))->not->toContain($pattern); +})->with('raw_reuse'); diff --git a/tests/Integration/test_monitor_request_output_wiring.php b/tests/Integration/test_monitor_request_output_wiring.php deleted file mode 100644 index e20dadd..0000000 --- a/tests/Integration/test_monitor_request_output_wiring.php +++ /dev/null @@ -1,41 +0,0 @@ - [ - "html_escape(get_request_var('downhosts'))", - "html_escape(get_request_var('mute'))", - "html_escape(get_request_var('tree'))", - "html_escape(get_request_var('site'))", - "html_escape(get_request_var('template'))", - "html_escape(get_request_var('size'))", - "html_escape(get_request_var('trim'))", - ], - __DIR__ . '/../../monitor_render.php' => [ - "rawurlencode(get_request_var('rfilter'))", - ], -]; - -foreach ($checks as $path => $patterns) { - $contents = file_get_contents($path); - - if ($contents === false) { - fwrite(STDERR, "Unable to read {$path}\n"); - exit(1); - } - - foreach ($patterns as $pattern) { - if (strpos($contents, $pattern) === false) { - fwrite(STDERR, "Missing expected output hardening: {$pattern}\n"); - exit(1); - } - } -} - -print "OK\n"; diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..8434908 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,14 @@ +beforeEach(function () { + $GLOBALS['__test_sql'] = []; + $GLOBALS['__test_settings'] = []; + test_set_request([]); +})->in(__DIR__); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..16d4009 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,101 @@ + dirname(__DIR__), + 'url_path' => '/cacti/', + 'cacti_version' => '1.2.999', +]; + +$GLOBALS['__test_request'] = []; +$GLOBALS['__test_settings'] = []; +$GLOBALS['__test_sql'] = []; + +if (!function_exists('get_request_var')) { + function get_request_var($name, $default = '') { + return $GLOBALS['__test_request'][$name] ?? $default; + } +} + +if (!function_exists('get_nfilter_request_var')) { + function get_nfilter_request_var($name, $default = '') { + return get_request_var($name, $default); + } +} + +if (!function_exists('read_config_option')) { + function read_config_option($name, $force = false) { + return $GLOBALS['__test_settings'][$name] ?? ''; + } +} + +if (!function_exists('read_user_setting')) { + function read_user_setting($name, $default = '', $force = false) { + return $GLOBALS['__test_settings'][$name] ?? $default; + } +} + +if (!function_exists('cacti_sizeof')) { + function cacti_sizeof($a) { return is_array($a) ? count($a) : 0; } +} + +if (!function_exists('db_qstr')) { + function db_qstr($s, $db_conn = false) { + return "'" . str_replace(["\\", "'"], ["\\\\", "\\'"], (string) $s) . "'"; + } +} + +if (!function_exists('db_qstr_rlike')) { + /* Mirrors Cacti core: cap the length, strip alternation and bounded + * repeats, then quote. */ + function db_qstr_rlike($s, $db_conn = false) { + $s = (string) $s; + + if (strlen($s) > 255) { + $s = substr($s, 0, 255); + } + + $s = str_replace(["\0", '|', '{', '}'], '', $s); + + return 'RLIKE ' . db_qstr($s, $db_conn); + } +} + +if (!function_exists('db_fetch_cell_prepared')) { + function db_fetch_cell_prepared($sql, $params = [], $col = '', $log = true, $db_conn = false) { + $GLOBALS['__test_sql'][] = ['sql' => $sql, 'params' => $params]; + + return $GLOBALS['__test_db_cell'] ?? ''; + } +} + +if (!function_exists('db_fetch_cell')) { + function db_fetch_cell($sql, $col = '', $log = true, $db_conn = false) { + $GLOBALS['__test_sql'][] = ['sql' => $sql, 'params' => []]; + + return $GLOBALS['__test_db_cell'] ?? ''; + } +} + +if (!function_exists('__')) { + function __($format, ...$args) { + if (count($args) > 1) { array_pop($args); } + + return $args === [] ? $format : vsprintf($format, $args); + } +} + +function test_set_request(array $vars): void { + $GLOBALS['__test_request'] = $vars + [ + 'crit' => 0, 'site' => 0, 'tree' => 0, 'grouping' => '', 'rfilter' => '', 'status' => '0', + ]; +} diff --git a/tests/e2e/test_monitor_no_raw_request_reuse.php b/tests/e2e/test_monitor_no_raw_request_reuse.php deleted file mode 100644 index 73be1e9..0000000 --- a/tests/e2e/test_monitor_no_raw_request_reuse.php +++ /dev/null @@ -1,40 +0,0 @@ - [ - "get_request_var('downhosts') . '\">'", - "get_request_var('site') . '\">'", - "get_request_var('template') . '\">'", - "get_request_var('size') . '\">'", - "get_request_var('trim') . '\">'", - ], - __DIR__ . '/../../monitor_render.php' => [ - "monitor.php?rfilter=' . get_request_var('rfilter')", - ], -]; - -foreach ($checks as $path => $patterns) { - $contents = file_get_contents($path); - - if ($contents === false) { - fwrite(STDERR, "Unable to read {$path}\n"); - exit(1); - } - - foreach ($patterns as $pattern) { - if (strpos($contents, $pattern) !== false) { - fwrite(STDERR, "Raw request reuse remains: {$pattern}\n"); - exit(1); - } - } -} - -print "OK\n"; diff --git a/tests/unit/DbFunctionsTest.php b/tests/unit/DbFunctionsTest.php new file mode 100644 index 0000000..216a384 --- /dev/null +++ b/tests/unit/DbFunctionsTest.php @@ -0,0 +1,59 @@ + '2']); + + expect(getTholdWhere())->toContain('td.thold_alert != 0 OR td.bl_alert > 0'); +}); + +it('selects the triggered clause for any other status', function () { + test_set_request(['status' => '0']); + + expect(getTholdWhere())->toContain('thold_fail_count >= td.thold_fail_trigger'); +}); + +it('builds an IN clause from a concatenated id list', function () { + $where = ''; + renderGroupConcat($where, ' AND ', 'h.id', '4,9,17'); + + expect($where)->toBe('(h.id IN(4,9,17) )'); +}); + +it('joins onto an existing clause rather than replacing it', function () { + $where = 'h.disabled = ""'; + renderGroupConcat($where, ' AND ', 'h.id', '4'); + + expect($where)->toStartWith('h.disabled = ""')->and($where)->toContain(' AND '); +}); + +it('adds nothing when the id list is empty', function () { + $where = ''; + renderGroupConcat($where, ' AND ', 'h.id', ''); + + expect($where)->toBe(''); +}); + +it('collapses the doubled commas GROUP_CONCAT can produce', function () { + $where = ''; + renderGroupConcat($where, ' AND ', 'h.id', ',,4,,9,,'); + + expect($where)->toBe('(h.id IN(4,9) )'); +}); + +it('appends the optional suffix', function () { + $where = ''; + renderGroupConcat($where, ' AND ', 'h.id', '4', 'OR h.id IS NULL'); + + expect($where)->toContain('OR h.id IS NULL'); +}); diff --git a/tests/unit/test_request_output_escaping.php b/tests/unit/test_request_output_escaping.php deleted file mode 100644 index a9cd09b..0000000 --- a/tests/unit/test_request_output_escaping.php +++ /dev/null @@ -1,19 +0,0 @@ - Date: Sun, 16 Aug 2026 23:49:58 -0700 Subject: [PATCH 13/16] ci: refuse package-supplied code in the root composer install Cacti core declares no scripts and ships no lockfile, so --no-plugins --no-scripts costs nothing and stops an unpinned dependency running as root on the integration runner. Signed-off-by: Thomas Vincent --- .github/workflows/plugin-ci-workflow.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index ed587fd..d1bf1c5 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -203,7 +203,9 @@ jobs: run: | cd ${{ github.workspace }}/cacti if [ -f composer.json ]; then - sudo composer install --prefer-dist --no-interaction --no-progress + # Cacti core ships no composer.lock and declares no scripts, so nothing + # is lost by refusing to run package-supplied code as root. + sudo composer install --prefer-dist --no-interaction --no-progress --no-plugins --no-scripts fi - name: Create Cacti config.php From 88173d76c4a68d0e0e170feffcaed44ca41371d5 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 17 Aug 2026 00:05:47 -0700 Subject: [PATCH 14/16] ci: run the unit suite on a PHP the test tooling supports The job pinned 8.1, but Pest 2 pulls brianium/paratest ^7.4, which requires 8.2, so composer install could never resolve. Verified in a Linux container across 8.1 to 8.4: 8.1 fails dependency resolution, 8.2 upward pass. Signed-off-by: Thomas Vincent --- .github/workflows/plugin-ci-workflow.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index d1bf1c5..88f23b6 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -81,7 +81,10 @@ jobs: - name: Install PHP uses: shivammathur/setup-php@b604ade2a87db23f8871b7182e69ec5e75effb45 # v2 with: - php-version: '8.1' + # 8.2 is the floor for the test tooling, not for the plugin: Pest 2 pulls + # brianium/paratest ^7.4, which requires 8.2. The plugin's own floor is + # covered by syntax-floor on 8.0 and by the 8.1 integration matrix entry. + php-version: '8.2' tools: composer - name: Install dev dependencies From 6cb7b41c533c77eeb23badae70bfd7b547d60bf0 Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 17 Aug 2026 00:25:20 -0700 Subject: [PATCH 15/16] ci: stop claiming PHP is analysed, and lint every workflow The CodeQL header said the PHP was covered by the syntax and integration jobs. Neither tracks a value from request to sink, so that reads as assurance the repository does not have. The paths filter also named one workflow file, so a change to codeql.yml never reached the workflow-lint job. Signed-off-by: Thomas Vincent --- .github/workflows/codeql.yml | 5 +++-- .github/workflows/plugin-ci-workflow.yml | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9dd80d5..8bc3f3a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -6,8 +6,9 @@ # | http://www.cacti.net/ | # +-------------------------------------------------------------------------+ # -# CodeQL has no PHP analysis, so this covers the plugin JavaScript only. The PHP is -# covered by the syntax and integration jobs in plugin-ci-workflow.yml. +# CodeQL has no PHP analysis, so this covers the plugin JavaScript only. Nothing in +# CI performs taint analysis on the PHP: plugin-ci-workflow.yml runs a parser check +# and an install smoke test, neither of which tracks a value from request to sink. name: "CodeQL" diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 88f23b6..8b1dcfa 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -28,7 +28,7 @@ on: - develop paths: - '**.php' - - '.github/workflows/plugin-ci-workflow.yml' + - '.github/workflows/**' - 'composer.json' - 'composer.lock' - 'phpunit.xml.dist' @@ -39,7 +39,7 @@ on: - develop paths: - '**.php' - - '.github/workflows/plugin-ci-workflow.yml' + - '.github/workflows/**' - 'composer.json' - 'composer.lock' - 'phpunit.xml.dist' From ce3e3f93fcf44fd6a9e031679d7306b4edce772c Mon Sep 17 00:00:00 2001 From: Thomas Vincent Date: Mon, 17 Aug 2026 18:39:13 -0700 Subject: [PATCH 16/16] test: run the suite from Cacti's Composer instead of the plugin's TheWitness asked on plugin_thold#773 for no composer.json in plugins and for testing to use Cacti's own Composer. The unit job now builds the image Cacti's tests/tools/docker_pest.sh builds and runs its locked Pest against a pinned runtime checkout, so the plugin contributes no dependency of its own. Dropping pestphp/pest also drops the reason the job was pinned to PHP 8.2. Signed-off-by: Thomas Vincent --- .github/workflows/plugin-ci-workflow.yml | 52 +++++++++++++++--------- composer.json | 21 ---------- 2 files changed, 33 insertions(+), 40 deletions(-) delete mode 100644 composer.json diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index 8b1dcfa..a57245e 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -29,8 +29,6 @@ on: paths: - '**.php' - '.github/workflows/**' - - 'composer.json' - - 'composer.lock' - 'phpunit.xml.dist' - 'tests/**' pull_request: @@ -40,8 +38,6 @@ on: paths: - '**.php' - '.github/workflows/**' - - 'composer.json' - - 'composer.lock' - 'phpunit.xml.dist' - 'tests/**' workflow_dispatch: @@ -72,29 +68,47 @@ jobs: args: -color unit-test: - name: Standalone test suite + name: Pest using Cacti Composer (Docker) runs-on: ubuntu-latest + timeout-minutes: 20 + steps: - name: Checkout monitor Plugin - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - - name: Install PHP - uses: shivammathur/setup-php@b604ade2a87db23f8871b7182e69ec5e75effb45 # v2 + - name: Checkout Cacti runtime + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 with: - # 8.2 is the floor for the test tooling, not for the plugin: Pest 2 pulls - # brianium/paratest ^7.4, which requires 8.2. The plugin's own floor is - # covered by syntax-floor on 8.0 and by the 8.1 integration matrix entry. - php-version: '8.2' - tools: composer + repository: Cacti/cacti + # Commit behind the annotated release/1.2.31 tag. + ref: 1e8eaca26b84b128c39ce8cc8ece42d7ff76aac1 + path: cacti-runtime - - name: Install dev dependencies - run: composer install --prefer-dist --no-progress --no-interaction --no-plugins --no-scripts + - name: Checkout Cacti test toolchain + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + with: + repository: Cacti/cacti + # Pins Composer, Pest, the lock file and Cacti's Docker test image. + ref: 298bd51eca843490fb90b27ada6b3fecc9b9a7d8 + path: cacti-toolchain + + # The same image Cacti's own tests/tools/docker_pest.sh builds. Composer and + # Pest come from Cacti's locked toolchain, so the plugin ships no + # composer.json and no parallel vendor directory. + - name: Build Cacti test image + run: | + docker build --tag cacti-web --file cacti-toolchain/docker/Dockerfile cacti-toolchain/docker + docker build --tag cacti-monitor-test --file cacti-toolchain/docker/Dockerfile.test cacti-toolchain - - name: Run the test suite - run: composer test + - name: Run Pest + run: | + docker run --rm \ + --volume "$PWD/cacti-runtime":/cacti \ + --volume "$PWD":/cacti/plugins/monitor \ + --user root \ + --entrypoint composer cacti-monitor-test \ + test -- --configuration=/cacti/plugins/monitor/phpunit.xml.dist - # compat = 1.2.15 implies Cacti 1.2.x, which requires PHP 8.0. The matrix - # below starts at 8.1, so it cannot catch syntax the declared floor rejects. syntax-floor: name: PHP 8.0 syntax floor runs-on: ubuntu-latest diff --git a/composer.json b/composer.json deleted file mode 100644 index 8d5f995..0000000 --- a/composer.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "cacti/plugin_monitor", - "description": "Monitor Plugin for Cacti", - "type": "cacti-plugin", - "license": "GPL-2.0-or-later", - "require": { - "php": ">=8.0" - }, - "require-dev": { - "pestphp/pest": "^2" - }, - "config": { - "allow-plugins": { - "pestphp/pest-plugin": true - } - }, - "scripts": { - "test": "pest", - "test:integration": "pest tests/Integration" - } -}