diff --git a/.github/workflows/plugin-ci-workflow.yml b/.github/workflows/plugin-ci-workflow.yml index fdbd61d..4669875 100644 --- a/.github/workflows/plugin-ci-workflow.yml +++ b/.github/workflows/plugin-ci-workflow.yml @@ -32,65 +32,7 @@ on: - develop jobs: - quality-checks: - name: PHP Quality (Lint, Stan, CS) - runs-on: ubuntu-latest - steps: - - name: Checkout Cacti - uses: actions/checkout@v4 - with: - repository: Cacti/cacti - path: cacti - - - name: Checkout Syslog Plugin - uses: actions/checkout@v4 - with: - path: cacti/plugins/syslog - - - name: Setup PHP 8.3 - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - extensions: intl, mysql, gd, ldap, gmp, xml, curl, json, mbstring - tools: php-cs-fixer, phpstan - - - name: Check PHP Syntax (Lint) - run: | - cd cacti/plugins/syslog - if find . -name '*.php' -not -path './vendor/*' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then - print "Syntax errors found!" - exit 1 - fi - - - name: Run PHP CS Fixer (Dry Run) - run: | - cd cacti/plugins/syslog - php-cs-fixer fix --dry-run --diff --ansi --config=../../../.php-cs-fixer.php . || true - - - name: Create PHPStan config - run: | - cd cacti/plugins/syslog - cat > phpstan.neon << 'EOF' - parameters: - level: 5 - paths: - - . - excludePaths: - - vendor/ - - locales/ - ignoreErrors: - - '#has invalid return type the\.#' - bootstrapFiles: - - ../../include/global.php - EOF - - - name: Run PHPStan Analysis - run: | - cd cacti/plugins/syslog - phpstan analyse --no-progress --error-format=github || true - integration-test: - needs: quality-checks runs-on: ${{ matrix.os }} strategy: @@ -110,7 +52,7 @@ jobs: ports: - 3306:3306 options: >- - --health-cmd="mysqladmin ping" + --health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -pcactiroot" --health-interval=10s --health-timeout=5s --health-retries=3 @@ -205,7 +147,7 @@ jobs: sed -i "s/'cacti'/'cacti'/g" ${{ github.workspace }}/cacti/plugins/syslog/config.php sed -i "s/'cactiuser'/'cactiuser'/g" ${{ github.workspace }}/cacti/plugins/syslog/config.php sed -i 's/\/\/\$/\$/g' ${{ github.workspace }}/cacti/plugins/syslog/config.php - sudo chmod 664 ${{ github.workspace }}/cacti/include/config.php + sudo chmod 664 ${{ github.workspace }}/cacti/plugins/syslog/config.php - name: Configure Apache run: | @@ -236,6 +178,59 @@ jobs: run: | cd ${{ github.workspace }}/cacti sudo php cli/plugin_manage.php --plugin=syslog --install --enable + + - name: Check PHP Syntax for Plugin + run: | + cd ${{ github.workspace }}/cacti/plugins/syslog + if find . -name '*.php' -exec php -l {} 2>&1 \; | grep -iv 'no syntax errors detected'; then + echo "Syntax errors found!" + exit 1 + fi + + - name: Create PHPStan config + run: | + cd ${{ github.workspace }}/cacti/plugins/syslog + cat > phpstan.neon << 'EOF' + parameters: + level: 5 + paths: + - . + excludePaths: + - vendor/ + - locales/ + ignoreErrors: + - '#has invalid return type the\.#' + bootstrapFiles: + - ../../include/global.php + EOF + + - name: Install PHPStan + run: | + cd ${{ github.workspace }}/cacti/plugins/syslog + composer require --dev phpstan/phpstan --with-all-dependencies || composer global require phpstan/phpstan + + - name: Run PHPStan Analysis + run: | + cd ${{ github.workspace }}/cacti/plugins/syslog + if [ -f vendor/bin/phpstan ]; then + vendor/bin/phpstan analyse --no-progress --error-format=github + else + phpstan analyse --no-progress --error-format=github + fi + + - name: Install PHP-CS-Fixer + run: | + cd ${{ github.workspace }}/cacti/plugins/syslog + composer require --dev friendsofphp/php-cs-fixer --with-all-dependencies || composer global require friendsofphp/php-cs-fixer + + - name: Run PHP-CS-Fixer Check + run: | + cd ${{ github.workspace }}/cacti/plugins/syslog + if [ -f vendor/bin/php-cs-fixer ]; then + vendor/bin/php-cs-fixer fix --dry-run --diff --format=github + else + php-cs-fixer fix --dry-run --diff --format=github + fi - name: Run Plugin Regression Tests run: | @@ -246,40 +241,39 @@ jobs: php "$test" done fi - - + - 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 - print "Cacti poller did not finish successfully" + echo "Cacti poller did not finish successfully" cat log/cacti.log exit 1 fi - + - name: Populate Syslog Test Data run: | sudo chmod +x ${{ github.workspace }}/cacti/plugins/syslog/.github/workflows/populate_syslog_incoming.sh cd ${{ github.workspace }}/cacti/plugins/syslog/.github/workflows sudo ./populate_syslog_incoming.sh - - name: force Syslog Plugin Poller run: | cd ${{ github.workspace }}/cacti sudo php plugins/syslog/syslog_process.php --debug if ! grep -q "SYSTEM SYSLOG STATS" log/cacti.log; then - print "Syslog plugin poller did not finish successfully" + echo "Syslog plugin 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 - print "=== Cacti Log ===" + echo "=== Cacti Log ===" sudo cat ${{ github.workspace }}/cacti/log/cacti.log fi + + diff --git a/.github/workflows/populate_syslog_incoming.sh b/.github/workflows/populate_syslog_incoming.sh index e51878f..bc7dd34 100644 --- a/.github/workflows/populate_syslog_incoming.sh +++ b/.github/workflows/populate_syslog_incoming.sh @@ -5,10 +5,10 @@ ITERATIONS=100 -SQL_ALERT_RULE_INSERT="INSERT INTO syslog_alert (id,hash,name,severity,method,level,num,type,enabled,repeat_alert,open_ticket,message,body,user,date,email,notify,command,notes) +SQL_ALERT_RULE_INSERT="REPLACE INTO syslog_alert (id,hash,name,severity,method,level,num,type,enabled,repeat_alert,open_ticket,message,body,user,date,email,notify,command,notes) VALUES (1,'8f440030d3425e37cb66e5df54902bb0','interface down alert',1,0,1,1,'messageb','on',0,'','interface down','admin',1767376990,NULL,0,NULL,NULL);" -SQL_REMOVAL_RULE_INSERT="INSERT INTO syslog_remove (id,hash,name,type,enabled,method,message,user,date,notes) +SQL_REMOVAL_RULE_INSERT="REPLACE INTO syslog_remove (id,hash,name,type,enabled,method,message,user,date,notes) VALUES (1,'0faf589f7b6b7da1bcec40b92340487d','exploded','messageb','on','del', 'the box exploded','admin',1767376604,NULL);" diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e74346..403053a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,7 @@ --- develop --- * issue#250: Fix date filter persistence by validating before shift_span detection -* issue#258: Execute CREATE TABLE SQL correctly during replication sync -* issue#278: Extract duplicated alert command execution paths in syslog_process_alerts -* issue#278: Extract alert command execution into shared helper in functions.php; command tokenization now uses preg_split (handles tabs and consecutive spaces); /bin/sh fallback for non-executable command templates removed (use absolute paths with execute bit set) +* issue#260: Replace eval-based callback execution in autocomplete handling * issue: Making changes to support Cacti 1.3 * issue: Don't use MyISAM for non-analytical tables * issue: The install advisor for Syslog was broken in current Cacti releases diff --git a/LICENSE b/LICENSE index 3de503f..d8cf7d4 100644 --- a/LICENSE +++ b/LICENSE @@ -277,7 +277,4 @@ YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -<<<<<<< Updated upstream -======= END OF TERMS AND CONDITIONS ->>>>>>> Stashed changes diff --git a/README.md b/README.md index 0e12b30..ab4afd9 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,9 @@ To use a dedicated DB first create a database in mysql and assign a user you wil $use_cacti_db = true; ``` -to +to -``console +```console $use_cacti_db = false; ``` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..24a4bb2 --- /dev/null +++ b/composer.json @@ -0,0 +1,10 @@ +{ + "require-dev": { + "pestphp/pest": "^4.4" + }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..3a837da --- /dev/null +++ b/composer.lock @@ -0,0 +1,3910 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "bc6810b603c69167bf3d5f71d45d0783", + "packages": [], + "packages-dev": [ + { + "name": "brianium/paratest", + "version": "v7.19.2", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", + "reference": "66e4f7910cecf67736bccf2b8bd53a2e3eb98bd9", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.3.0", + "jean85/pretty-package-versions": "^2.1.1", + "php": "~8.3.0 || ~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1", + "phpunit/php-file-iterator": "^6.0.1 || ^7", + "phpunit/php-timer": "^8 || ^9", + "phpunit/phpunit": "^12.5.14 || ^13.0.5", + "sebastian/environment": "^8.0.3 || ^9", + "symfony/console": "^7.4.7 || ^8.0.7", + "symfony/process": "^7.4.5 || ^8.0.5" + }, + "require-dev": { + "doctrine/coding-standard": "^14.0.0", + "ext-pcntl": "*", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.40", + "phpstan/phpstan-deprecation-rules": "^2.0.4", + "phpstan/phpstan-phpunit": "^2.0.16", + "phpstan/phpstan-strict-rules": "^2.0.10", + "symfony/filesystem": "^7.4.6 || ^8.0.6" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.19.2" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2026-03-09T14:33:17+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.6", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=14" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^14", + "phpstan/phpstan": "1.4.10 || 2.1.30", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.6" + }, + "time": "2026-02-07T07:09:04+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2025-08-14T07:29:31+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.1", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "reference": "a1ed3fa530fd60bc515f9303e8520fcb7d4bd935", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.2", + "laravel/framework": "^11.48.0 || ^12.52.0", + "laravel/pint": "^1.27.1", + "orchestra/testbench-core": "^9.12.0 || ^10.9.0", + "pestphp/pest": "^3.8.5 || ^4.4.1 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.3 || ^9.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-02-17T17:33:08+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "pestphp/pest", + "version": "v4.4.3", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "e6ab897594312728ef2e32d586cb4f6780b1b495" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/e6ab897594312728ef2e32d586cb4f6780b1b495", + "reference": "e6ab897594312728ef2e32d586cb4f6780b1b495", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.19.2", + "nunomaduro/collision": "^8.9.1", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest-plugin": "^4.0.0", + "pestphp/pest-plugin-arch": "^4.0.0", + "pestphp/pest-plugin-mutate": "^4.0.1", + "pestphp/pest-plugin-profanity": "^4.2.1", + "php": "^8.3.0", + "phpunit/phpunit": "^12.5.14", + "symfony/process": "^7.4.5|^8.0.5" + }, + "conflict": { + "filp/whoops": "<2.18.3", + "phpunit/phpunit": ">12.5.14", + "sebastian/exporter": "<7.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "pestphp/pest-dev-tools": "^4.1.0", + "pestphp/pest-plugin-browser": "^4.3.0", + "pestphp/pest-plugin-type-coverage": "^4.0.3", + "psy/psysh": "^0.12.21" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Shard", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v4.4.3" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2026-03-21T13:14:39+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.3" + }, + "conflict": { + "pestphp/pest": "<4.0.0" + }, + "require-dev": { + "composer/composer": "^2.8.10", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", + "keywords": [ + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-08-20T12:35:58+00:00" + }, + { + "name": "pestphp/pest-plugin-arch", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "reference": "25bb17e37920ccc35cbbcda3b00d596aadf3e58d", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "ta-tikoma/phpunit-architecture-test": "^0.8.5" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Arch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Arch plugin for Pest PHP.", + "keywords": [ + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-20T13:10:51+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v4.0.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", + "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.6.1", + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-08-21T20:19:25+00:00" + }, + { + "name": "pestphp/pest-plugin-profanity", + "version": "v4.2.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-profanity.git", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^4.0.0", + "php": "^8.3" + }, + "require-dev": { + "faissaloux/pest-plugin-inside": "^1.9", + "pestphp/pest": "^4.0.0", + "pestphp/pest-dev-tools": "^4.0.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Profanity\\Plugin" + ] + } + }, + "autoload": { + "psr-4": { + "Pest\\Profanity\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Profanity Plugin", + "keywords": [ + "framework", + "pest", + "php", + "plugin", + "profanity", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + }, + "time": "2025-12-08T00:13:17+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^2.0", + "phpstan/phpdoc-parser": "^2.0", + "webmozart/assert": "^1.9.1 || ^2" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26", + "shipmonk/dead-code-detector": "^0.5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" + }, + "time": "2026-03-18T20:49:53+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9", + "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev", + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0" + }, + "time": "2026-01-06T21:53:42+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.2", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a", + "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2" + }, + "time": "2026-01-25T14:56:51+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "reference": "b015312f28dd75b75d3422ca37dff2cd1a565e8d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-file-iterator": "^6.0", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-02-06T06:01:44+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.14", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/47283cfd98d553edcb1353591f4e255dc1bb61f0", + "reference": "47283cfd98d553edcb1353591f4e255dc1bb61f0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.3", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.4", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.0.3", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.14" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2026-02-18T12:38:40+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "reference": "90f41072d220e5c40df6e8635f5dafba2d9d4d04", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2025-09-14T09:36:45+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "reference": "6a7de5df2e094f9a80b40a522391a7e6022df5f6", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:28:48+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "reference": "7b8842c2d8e85d0c3a5831236bf5869af6ab2a11", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-03-15T07:05:40+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:16:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-29T11:29:25+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:57:12+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/console", + "version": "v8.0.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "reference": "15ed9008a4ebe2d6a78e4937f74e0c13ef2e618a", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4|^8.0" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.0.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-06T14:06:22+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "reference": "441404f09a54de6d1bd6ad219e088cdf4c91f97c", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-29T09:41:02+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/process", + "version": "v8.0.5", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "reference": "b5f3aa6762e33fd95efbaa2ec4f4bc9fdd16d674", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.0.5" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T15:08:38+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", + "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:30:57+00:00" + }, + { + "name": "symfony/string", + "version": "v8.0.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.0.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-02-09T10:14:57+00:00" + }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.7", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8", + "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7" + }, + "time": "2026-02-17T17:25:14+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "webmozart/assert", + "version": "2.1.6", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", + "php": "^8.2" + }, + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-feature/2-0": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + }, + { + "name": "Woody Gilk", + "email": "woody.gilk@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/2.1.6" + }, + "time": "2026-02-27T10:28:38+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/functions.php b/functions.php index 777959c..8796de0 100644 --- a/functions.php +++ b/functions.php @@ -22,24 +22,6 @@ +-------------------------------------------------------------------------+ */ -function syslog_apply_selected_items_action($selected_items, $drp_action, $action_map, $export_action = '', $export_items = '') { - if ($selected_items != false) { - if (isset($action_map[$drp_action])) { - $action_function = $action_map[$drp_action]; - - if (function_exists($action_function)) { - foreach($selected_items as $selected_item) { - $action_function($selected_item); - } - } else { - cacti_log("SYSLOG ERROR: Bulk action function '$action_function' not found.", false, 'SYSTEM'); - } - } elseif ($export_action != '' && $drp_action == $export_action) { - $_SESSION['exporter'] = rawurlencode(serialize($selected_items)); - } - } -} - function syslog_include_js() { global $config; ?> @@ -144,6 +126,61 @@ function syslog_sendemail($to, $from, $subject, $message, $smsmessage = '') { } } +/** + * Dispatch a bulk action on selected items from a form submission. + * + * @param array|false $selected_items Unserialized selected item IDs, or false + * @param int $drp_action The dropdown action identifier + * @param array $action_map Map of action IDs to handler function names + * @param string $export_action The action ID that triggers CSV export + * @param string $export_items Comma-separated item IDs for export + * + * @return void + */ +/** + * syslog_draw_bulk_action_confirm - Renders the confirmation list for bulk actions. + * + * @param (string) $drp_action The action ID + * @param (string) $item_list The HTML
  • list of items + * @param (string) $item_type_label The plural label for the items (e.g. 'Syslog Alert Rule(s)') + * @param (array) $action_labels Map of action IDs to their infinitive verbs (e.g. 1 => 'Delete') + */ +function syslog_draw_bulk_action_confirm($drp_action, $item_list, $item_type_label, $action_labels) { + $action_verb = $action_labels[$drp_action] ?? ''; + + print " + +

    " . __esc("Click 'Continue' to %s the following %s.", $action_verb, $item_type_label, 'syslog') . "

    +
    + + \n"; +} + +function syslog_apply_selected_items_action($selected_items, $drp_action, $action_map, $export_action = '', $export_items = '') { + if ($selected_items != false) { + if (isset($action_map[$drp_action])) { + $action_function = $action_map[$drp_action]; + + if (function_exists($action_function)) { + foreach($selected_items as $selected_item) { + try { + $action_function($selected_item); + } catch (Exception $e) { + cacti_log("SYSLOG ERROR: Action '$action_function' failed for item '$selected_item': " . $e->getMessage(), false, 'SYSTEM'); + } + } + } else { + cacti_log("SYSLOG ERROR: Bulk action function '$action_function' not found.", false, 'SYSTEM'); + } + } elseif ($export_action != '' && $drp_action == $export_action) { + /* Re-serialize the sanitized array and URL-encode so the value is + * safe to embed in a JS document.location string (avoids injection + * via the raw request value that $export_items carries). */ + $_SESSION['exporter'] = rawurlencode(serialize($selected_items)); + } + } +} + function syslog_get_import_xml_payload($redirect_url) { if (trim(get_nfilter_request_var('import_text')) != '') { /* textbox input */ @@ -210,19 +247,19 @@ function syslog_traditional_manage() { /* determine the oldest date to retain */ if (read_config_option('syslog_retention') > 0) { - $retention = date('Y-m-d', time() - (86400 * read_config_option('syslog_retention'))); + $retention = gmdate('Y-m-d', time() - (86400 * read_config_option('syslog_retention'))); } else { - $retention = date('Y-m-d', time() - (30 * 86400)); + $retention = gmdate('Y-m-d', time() - (30 * 86400)); set_config_option('syslog_retention', '30'); } /* delete from the main syslog table first */ - syslog_db_execute("DELETE FROM `" . $syslogdb_default . "`.`syslog` WHERE logtime < '$retention'"); + syslog_db_execute_prepared("DELETE FROM `" . $syslogdb_default . "`.`syslog` WHERE logtime < ?", array($retention)); $syslog_deleted = db_affected_rows($syslog_cnn); /* now delete from the syslog removed table */ - syslog_db_execute("DELETE FROM `" . $syslogdb_default . "`.`syslog_removed` WHERE logtime < '$retention'"); + syslog_db_execute_prepared("DELETE FROM `" . $syslogdb_default . "`.`syslog_removed` WHERE logtime < ?", array($retention)); $syslog_deleted += db_affected_rows($syslog_cnn); @@ -251,29 +288,23 @@ function syslog_partition_manage() { } /** - * Validate tables that support partition maintenance. + * syslog_partition_table_allowed - validate that the table being partitioned + * is in our approved list. * - * Any value added to the allowlist MUST match ^[a-z_]+$ so it is safe - * for identifier interpolation in DDL statements (MySQL does not support - * parameter binding for identifiers). + * @param (string) The table name + * + * @return (bool) True if allowed, False otherwise */ function syslog_partition_table_allowed($table) { - if (!in_array($table, array('syslog', 'syslog_removed'), true)) { - return false; - } - - /* Defense-in-depth: reject values unsafe for identifier interpolation. */ - if (!preg_match('/^[a-z_]+$/', $table)) { - return false; + if (in_array($table, array('syslog', 'syslog_removed'), true)) { + return (bool)preg_match('/^[a-z_]+$/', $table); } - return true; + return false; } /** - * Create a new partition for the specified table. - * - * @return bool true on success, false on lock failure or disallowed table. + * This function will create a new partition for the specified table. */ function syslog_partition_create($table) { global $syslogdb_default; @@ -282,135 +313,127 @@ function syslog_partition_create($table) { return false; } - /* Hash to guarantee the lock name stays within MySQL's 64-byte limit. */ - $lock_name = substr(hash('sha256', $syslogdb_default . '.syslog_partition_create.' . $table), 0, 60); + /* determine the format of the table name */ + $time = time(); + $cformat = 'd' . gmdate('Ymd', $time); + $lnow = gmdate('Y-m-d', $time+86400); - /* - * 10-second timeout is sufficient: partition maintenance runs once per - * poller cycle (typically 5 minutes), so sustained contention is not - * expected. A failure is logged so monitoring can detect repeated misses. - */ - $locked = syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 10)', array($lock_name)); - - if ($locked === null) { - /* NULL means the GET_LOCK call itself failed, not just contention. */ - cacti_log("SYSLOG: GET_LOCK call failed for partition create on '$table'", false, 'SYSTEM'); + /* validate DDL interpolation values to prevent injection */ + if (!preg_match('/^d\d{8}$/', $cformat) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $lnow)) { + cacti_log('SYSLOG ERROR: Invalid partition format values detected', false, 'SYSTEM'); return false; } - if ((int)$locked !== 1) { - cacti_log("SYSLOG: Unable to acquire partition create lock for '$table'", false, 'SYSTEM'); - return false; - } + $exists = syslog_db_fetch_row_prepared("SELECT * + FROM `information_schema`.`partitions` + WHERE table_schema = ? + AND partition_name = ? + AND table_name = ? + ORDER BY partition_ordinal_position", + array($syslogdb_default, $cformat, $table) + ); - try { - /* determine the format of the table name */ - $time = time(); - $cformat = 'd' . date('Ymd', $time); - $lnow = date('Y-m-d', $time+86400); + if (!cacti_sizeof($exists)) { + $lock_name = hash('sha256', $syslogdb_default . 'syslog_partition_create.' . $table); - $exists = syslog_db_fetch_row_prepared("SELECT * - FROM `information_schema`.`partitions` - WHERE table_schema = ? - AND partition_name = ? - AND table_name = ? - ORDER BY partition_ordinal_position", - array($syslogdb_default, $cformat, $table)); + try { + $locked = syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 10)', array($lock_name)); - if (!cacti_sizeof($exists)) { - cacti_log("SYSLOG: Creating new partition '$cformat'", false, 'SYSTEM'); + if ((int)$locked !== 1) { + cacti_log("SYSLOG WARNING: Failed to acquire partition create lock for '$table'", false, 'SYSTEM'); + return false; + } - syslog_debug("Creating new partition '$cformat'"); + $recheck = syslog_db_fetch_row_prepared("SELECT * + FROM `information_schema`.`partitions` + WHERE table_schema = ? + AND partition_name = ? + AND table_name = ? + ORDER BY partition_ordinal_position", + array($syslogdb_default, $cformat, $table) + ); - /* - * MySQL does not support parameter binding for DDL identifiers - * or partition definitions. $table is safe because it passed - * syslog_partition_table_allowed() (two-value allowlist plus - * regex guard). $cformat and $lnow derive from date() and - * contain only digits, hyphens, and the letter 'd'. - */ - syslog_db_execute("ALTER TABLE `" . $syslogdb_default . "`.`$table` REORGANIZE PARTITION dMaxValue INTO ( + if (cacti_sizeof($recheck)) { + return true; + } + + cacti_log("SYSLOG: Creating new partition '$cformat' for table '$table'", false, 'SYSTEM'); + + syslog_debug("Creating new partition '$cformat' for table '$table'"); + + /* MySQL does not support parameter binding for DDL statements; + $cformat and $lnow are validated above via regex */ + syslog_db_execute_prepared("ALTER TABLE `" . $syslogdb_default . "`.`$table` REORGANIZE PARTITION dMaxValue INTO ( PARTITION $cformat VALUES LESS THAN (TO_DAYS('$lnow')), PARTITION dMaxValue VALUES LESS THAN MAXVALUE)"); + } finally { + syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', array($lock_name)); } - } finally { - syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', array($lock_name)); } return true; } /** - * Remove old partitions for the specified table. + * This function will remove all old partitions for the specified table. */ function syslog_partition_remove($table) { global $syslogdb_default; if (!syslog_partition_table_allowed($table)) { - cacti_log("SYSLOG: partition_remove called with disallowed table '$table'", false, 'SYSTEM'); + cacti_log("SYSLOG ERROR: Attempt to remove partitions from disallowed table '$table'", false, 'SYSTEM'); return 0; } - $lock_name = substr(hash('sha256', $syslogdb_default . '.syslog_partition_remove.' . $table), 0, 60); - - $locked = syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 10)', array($lock_name)); - - if ($locked === null) { - cacti_log("SYSLOG: GET_LOCK call failed for partition remove on '$table'", false, 'SYSTEM'); - return 0; - } + $syslog_deleted = 0; + $days = read_config_option('syslog_retention'); - if ((int)$locked !== 1) { - cacti_log("SYSLOG: Unable to acquire partition remove lock for '$table'", false, 'SYSTEM'); - return 0; - } + if ($days > 0) { + $lock_name = hash('sha256', $syslogdb_default . 'syslog_partition_remove.' . $table); - $syslog_deleted = 0; + try { + $locked = syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 10)', array($lock_name)); - try { - $number_of_partitions = syslog_db_fetch_assoc_prepared("SELECT * - FROM `information_schema`.`partitions` - WHERE table_schema = ? AND table_name = ? - ORDER BY partition_ordinal_position", - array($syslogdb_default, $table)); + if ((int)$locked !== 1) { + cacti_log("SYSLOG WARNING: Failed to acquire partition remove lock for '$table'", false, 'SYSTEM'); + return $syslog_deleted; + } - $days = read_config_option('syslog_retention'); + $number_of_partitions = syslog_db_fetch_assoc_prepared("SELECT * + FROM `information_schema`.`partitions` + WHERE table_schema = ? + AND table_name = ? + ORDER BY partition_ordinal_position", + array($syslogdb_default, $table) + ); - syslog_debug("There are currently '" . sizeof($number_of_partitions) . "' Syslog Partitions, We will keep '$days' of them."); + syslog_debug("There are currently '" . sizeof($number_of_partitions) . "' Syslog Partitions for '$table', We will keep '$days' of them."); - if ($days > 0) { $user_partitions = sizeof($number_of_partitions) - 1; if ($user_partitions >= $days) { $i = 0; while ($user_partitions > $days) { $oldest = $number_of_partitions[$i]; - cacti_log("SYSLOG: Removing old partition '" . $oldest['PARTITION_NAME'] . "'", false, 'SYSTEM'); + cacti_log("SYSLOG: Removing old partition '" . $oldest['PARTITION_NAME'] . "' from table '$table'", false, 'SYSTEM'); - syslog_debug("Removing partition '" . $oldest['PARTITION_NAME'] . "'"); + syslog_debug("Removing partition '" . $oldest['PARTITION_NAME'] . "' from table '$table'"); - syslog_db_execute("ALTER TABLE `" . $syslogdb_default . "`.`$table` DROP PARTITION " . $oldest['PARTITION_NAME']); + syslog_db_execute_prepared("ALTER TABLE `" . $syslogdb_default . "`.`$table` DROP PARTITION " . $oldest['PARTITION_NAME']); $i++; $user_partitions--; $syslog_deleted++; } } + } finally { + syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', array($lock_name)); } - } finally { - syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', array($lock_name)); } return $syslog_deleted; } -/* - * syslog_partition_check is a read-only SELECT against information_schema. - * It does not execute DDL, so it does not need the named lock that - * syslog_partition_create and syslog_partition_remove acquire. External - * serialization is provided by the poller cycle calling - * syslog_partition_manage(). - */ function syslog_partition_check($table) { global $syslogdb_default; @@ -425,15 +448,18 @@ function syslog_partition_check($table) { /* find date of last partition */ $last_part = syslog_db_fetch_cell_prepared("SELECT PARTITION_NAME FROM `information_schema`.`partitions` - WHERE table_schema = ? AND table_name = ? + WHERE table_schema = ? + AND table_name = ? ORDER BY partition_ordinal_position DESC LIMIT 1,1", - array($syslogdb_default, $table)); + array($syslogdb_default, $table) + ); $lformat = str_replace('d', '', $last_part); - $cformat = date('Ymd'); + $cformat = gmdate('Ymd'); if ($cformat > $lformat) { + return true; } else { return false; @@ -448,22 +474,16 @@ function syslog_check_changed($request, $session) { } } -function syslog_remove_items($table, $max_seq) { +function syslog_remove_items($table, $uniqueID) { global $config, $syslog_cnn, $syslog_incoming_config; global $syslogdb_default; syslog_debug('-------------------------------------------------------------------------------------'); syslog_debug('Processing Removal Rules...'); - if ($table == 'syslog') { - $rows = syslog_db_fetch_assoc("SELECT * - FROM `" . $syslogdb_default . "`.`syslog_remove` - WHERE enabled = 'on'"); - } else { - $rows = syslog_db_fetch_assoc('SELECT * - FROM `' . $syslogdb_default . '`.`syslog_remove` - WHERE enabled="on"'); - } + $rows = syslog_db_fetch_assoc('SELECT * + FROM `' . $syslogdb_default . '`.`syslog_remove` + WHERE enabled="on"'); syslog_debug(sprintf('Found %5s - Removal Rule(s) to process', cacti_sizeof($rows))); @@ -473,155 +493,117 @@ function syslog_remove_items($table, $max_seq) { if ($table == 'syslog_incoming') { $total = syslog_db_fetch_cell_prepared('SELECT count(*) FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `status` = 1 - AND `seq` <= ?', - array($max_seq)); + WHERE `status` = ?', array($uniqueID)); } else { $total = 0; } if (cacti_sizeof($rows)) { foreach($rows as $remove) { - $sql_where = ''; - $params = array(); - - if ($remove['type'] == 'facility') { - if ($table == 'syslog_incoming') { - $sql_where = 'WHERE `' . $syslog_incoming_config['facilityField'] . '` = ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = $remove['message']; - $params[] = $max_seq; - } else { - $facility_id = syslog_db_fetch_cell_prepared('SELECT facility_id - FROM `' . $syslogdb_default . '`.`syslog_facilities` - WHERE facility = ?', array($remove['message'])); - - if (!empty($facility_id)) { - $sql_where = 'WHERE facility_id = ?'; - $params[] = $facility_id; - } - } - } else if ($remove['type'] == 'program') { - if ($table == 'syslog_incoming') { - $sql_where = 'WHERE `program` = ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = $remove['message']; - $params[] = $max_seq; - } else { - $program_id = syslog_db_fetch_cell_prepared('SELECT program_id - FROM `' . $syslogdb_default . '`.`syslog_programs` - WHERE program = ?', array($remove['message'])); - - if (!empty($program_id)) { - $sql_where = 'WHERE program_id = ?'; - $params[] = $program_id; - } - } - } elseif ($remove['type'] == 'host') { - if ($table == 'syslog_incoming') { - $sql_where = 'WHERE `host` = ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = $remove['message']; - $params[] = $max_seq; - } else { - $host_id = syslog_db_fetch_cell_prepared('SELECT host_id - FROM `' . $syslogdb_default . '`.`syslog_hosts` - WHERE host = ?', array($remove['message'])); + $sql = ''; + $sql1 = ''; + $params = array(); + $params1 = array(); + + $column_map = array( + 'facility' => 'facility_id', + 'host' => 'host_id', + 'program' => 'program_id', + 'messageb' => $syslog_incoming_config['textField'], + 'messagec' => $syslog_incoming_config['textField'], + 'messagee' => $syslog_incoming_config['textField'], + ); - if (!empty($host_id)) { - $sql_where = 'WHERE host_id = ?'; - $params[] = $host_id; - } - } - } elseif ($remove['type'] == 'messageb') { - if ($table == 'syslog_incoming') { - $sql_where = 'WHERE `' . $syslog_incoming_config['textField'] . '` LIKE ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = $remove['message'] . '%'; - $params[] = $max_seq; - } else { - $sql_where = 'WHERE message LIKE ?'; - $params[] = $remove['message'] . '%'; - } - } elseif ($remove['type'] == 'messagec') { - if ($table == 'syslog_incoming') { - $sql_where = 'WHERE `' . $syslog_incoming_config['textField'] . '` LIKE ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = '%' . $remove['message'] . '%'; - $params[] = $max_seq; - } else { - $sql_where = 'WHERE message LIKE ?'; - $params[] = '%' . $remove['message'] . '%'; - } - } elseif ($remove['type'] == 'messagee') { - if ($table == 'syslog_incoming') { - $sql_where = 'WHERE `' . $syslog_incoming_config['textField'] . '` LIKE ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = '%' . $remove['message']; - $params[] = $max_seq; - } else { - $sql_where = 'WHERE message LIKE ?'; - $params[] = '%' . $remove['message']; + if ($table == 'syslog_incoming') { + $column = $column_map[$remove['type']] ?? 'message'; + if ($remove['type'] == 'host') { + $column = 'si.host'; + } elseif ($remove['type'] == 'program') { + $column = 'si.program'; } - } elseif ($remove['type'] == 'sql') { - if ($table == 'syslog_incoming') { - $sql_where = 'WHERE (' . $remove['message'] . ') - AND `status` = 1 - AND `seq` <= ?'; - $params[] = $max_seq; - } else { - $sql_where = 'WHERE (' . $remove['message'] . ')'; - } - } + $filter = syslog_build_match_filter($remove['type'], $remove['message'], $column); - if ($sql_where != '') { if ($remove['method'] != 'del') { - if ($table == 'syslog_incoming') { - syslog_db_execute_prepared('INSERT INTO `' . $syslogdb_default . '`.`syslog_removed` - (logtime, priority_id, facility_id, program_id, host_id, message) - SELECT si.logtime, si.priority_id, si.facility_id, sp.program_id, sh.host_id, si.message + $sql1 = 'INSERT INTO `' . $syslogdb_default . '`.`syslog_removed` + (logtime, priority_id, facility_id, program_id, host_id, message) + SELECT logtime, priority_id, facility_id, program_id, host_id, message + FROM (SELECT si.logtime, si.priority_id, si.facility_id, spg.program_id, sh.host_id, si.message FROM `' . $syslogdb_default . '`.`syslog_incoming` AS si + INNER JOIN `' . $syslogdb_default . '`.`syslog_facilities` AS sf + ON sf.facility_id = si.facility_id + INNER JOIN `' . $syslogdb_default . '`.`syslog_priorities` AS sp + ON sp.priority_id = si.priority_id + INNER JOIN `' . $syslogdb_default . '`.`syslog_programs` AS spg + ON spg.program = si.program INNER JOIN `' . $syslogdb_default . '`.`syslog_hosts` AS sh ON sh.host = si.host - INNER JOIN `' . $syslogdb_default . '`.`syslog_programs` AS sp - ON sp.program = si.program ' . $sql_where, $params); - } else { - syslog_db_execute_prepared('INSERT INTO `' . $syslogdb_default . '`.`syslog_removed` + WHERE ' . $filter['sql'] . ' + AND `status` = ? + ) AS merge'; + $params1 = array_merge($filter['params'], array($uniqueID)); + } + + $delete_column = $column_map[$remove['type']] ?? $syslog_incoming_config['textField']; + if ($remove['type'] == 'facility') { + $delete_column = $syslog_incoming_config['facilityField']; + } elseif ($remove['type'] == 'host') { + $delete_column = $syslog_incoming_config['hostField']; + } elseif ($remove['type'] == 'program') { + $delete_column = $syslog_incoming_config['programField']; + } + $delete_filter = syslog_build_match_filter($remove['type'], $remove['message'], $delete_column); + + $sql = 'DELETE + FROM `' . $syslogdb_default . '`.`syslog_incoming` + WHERE ' . $delete_filter['sql'] . ' + AND `status` = ?'; + $params = array_merge($delete_filter['params'], array($uniqueID)); + } else { + $column = $column_map[$remove['type']] ?? 'message'; + $filter = syslog_build_match_filter($remove['type'], $remove['message'], $column); + + if (!empty($filter['sql'])) { + if ($remove['method'] != 'del') { + $sql1 = 'INSERT INTO `' . $syslogdb_default . '`.`syslog_removed` (logtime, priority_id, facility_id, program_id, host_id, message) SELECT logtime, priority_id, facility_id, program_id, host_id, message - FROM `' . $syslogdb_default . '`.`syslog` ' . $sql_where, $params); + FROM `' . $syslogdb_default . '`.`syslog` + WHERE ' . $filter['sql']; + $params1 = $filter['params']; } + $sql = 'DELETE FROM `' . $syslogdb_default . '`.`syslog` + WHERE ' . $filter['sql']; + $params = $filter['params']; + } + } + + if ($sql != '' || $sql1 != '') { + $debugm = ''; + /* process the removal rule first */ + if ($sql1 != '') { + syslog_db_execute_prepared($sql1, $params1); $xferred += db_affected_rows($syslog_cnn); } - if ($table == 'syslog_incoming') { - syslog_db_execute_prepared('DELETE FROM `' . $syslogdb_default . '`.`syslog_incoming` ' . $sql_where, $params); + /* now delete the remainder that match */ + syslog_db_execute_prepared($sql, $params); + $affected = db_affected_rows($syslog_cnn); + $removed += $affected; + + if ($sql1 != '') { + $debugm = sprintf('Moved %5s - ', $xferred); } else { - syslog_db_execute_prepared('DELETE FROM `' . $syslogdb_default . '`.`syslog` ' . $sql_where, $params); + $debugm = sprintf('Deleted %5s - ', $removed); } - $removed += db_affected_rows($syslog_cnn); + syslog_debug($debugm . 'Message' . ($affected == 1 ? '' : 's' ) . + " for removal rule '" . $remove['name'] . "'"); } } } - syslog_debug(sprintf('Removed %5s - Record(s) from ' . $table, $removed)); - syslog_debug(sprintf('Xferred %5s - Record(s) to the syslog_removed table', $xferred)); - return array('removed' => $removed, 'xferred' => $xferred); } @@ -630,60 +612,61 @@ function syslog_remove_items($table, $max_seq) { * it supports both the legacy as well as the new approach to controlling these * colors. */ -function syslog_log_row_color($severity, $tip_title) { - switch($severity) { - case '': - case '0': - $class = 'logInfo'; - break; - case '1': - $class = 'logWarning'; - break; - case '2': - $class = 'logAlert'; - break; +/** + * syslog_get_row_class - Maps a priority or severity level to a CSS class. + * + * @param (int) $level The priority (0-7) or severity (0-2) level + * @param (string) $type The level type ('priority' or 'severity') + * + * @return (string) The CSS class name + */ +function syslog_get_row_class($level, $type = 'priority') { + if ($type == 'severity') { + switch ($level) { + case '1': return 'logWarning'; + case '2': return 'logAlert'; + default: return 'logInfo'; + } } - print "\n"; -} + $classes = array( + '0' => 'logEmergency', + '1' => 'logAlert', + '2' => 'logCritical', + '3' => 'logError', + '4' => 'logWarning', + '5' => 'logNotice', + '6' => 'logInfo', + '7' => 'logDebug' + ); -/** function syslog_row_color() - * This function set's the CSS for each row of the syslog table as it is displayed - * it supports both the legacy as well as the new approach to controlling these - * colors. -*/ -function syslog_row_color($priority, $message) { - switch($priority) { - case '0': - $class = 'logEmergency'; - break; - case '1': - $class = 'logAlert'; - break; - case '2': - $class = 'logCritical'; - break; - case '3': - $class = 'logError'; - break; - case '4': - $class = 'logWarning'; - break; - case '5': - $class = 'logNotice'; - break; - case '6': - $class = 'logInfo'; - break; - case '7': - $class = 'logDebug'; - break; - } + return $classes[$level] ?? 'logInfo'; +} - print ""; +/** + * syslog_print_row - Prints a with the appropriate class and title. + * + * @param (int) $level The level + * @param (string) $message The title/tip content + * @param (string) $type The level type + * @param (string) $extra Additional CSS classes + */ +function syslog_print_row($level, $message, $type = 'priority', $extra = '') { + $class = syslog_get_row_class($level, $type); + $base_classes = ($type == 'priority') ? 'selectable syslogRow syslog-detail-row' : 'selectable'; + + print ""; return $class; } +function syslog_log_row_color($severity, $tip_title) { + return syslog_print_row($severity, $tip_title, 'severity'); +} + +function syslog_row_color($priority, $message) { + return syslog_print_row($priority, $message, 'priority'); +} + function sql_hosts_where($tab) { global $hostfilter, $hostfilter_log, $syslog_incoming_config; global $syslogdb_default; @@ -912,67 +895,26 @@ function syslog_manage_items($from_table, $to_table) { $sql_sel = ''; $sql_dlt = ''; + $params = array(); + + $column_map = array( + 'facility' => 'facility_id', + 'host' => 'host_id', + 'program' => 'program_id', + 'messageb' => $syslog_incoming_config['textField'], + 'messagec' => $syslog_incoming_config['textField'], + 'messagee' => $syslog_incoming_config['textField'], + ); - if ($remove['type'] == 'facility') { - if ($remove['method'] != 'del') { - $sql_sel = "SELECT seq FROM `" . $syslogdb_default . "`. $from_table - WHERE facility_id IN - (SELECT distinct facility_id FROM `". $syslogdb_default . "`syslog_facilities - WHERE facility ='". $remove['message']."')"; - } else { - $sql_dlt = "DELETE FROM `" . $syslogdb_default . "`. $from_table - WHERE facility_id IN - (SELECT distinct facility_id FROM `". $syslogdb_default . "`syslog_facilities - WHERE facility ='". $remove['message']."')"; - } - - } elseif ($remove['type'] == 'host') { - if ($remove['method'] != 'del') { - $sql_sel = "SELECT seq - FROM `" . $syslogdb_default . "`. $from_table - WHERE host_id in - (SELECT distinct host_id FROM `". $syslogdb_default . "`syslog_hosts - WHERE host ='". $remove['message']."')"; - } else { - $sql_dlt = "DELETE FROM `" . $syslogdb_default . "`. $from_table - WHERE host_id in - (SELECT distinct host_id FROM `". $syslogdb_default . "`syslog_hosts - WHERE host ='". $remove['message']."')"; - } - } elseif ($remove['type'] == 'messageb') { - if ($remove['method'] != 'del') { - $sql_sel = "SELECT seq FROM `" . $syslogdb_default . "`. $from_table - WHERE message LIKE '" . $remove['message'] . "%' "; - } else { - $sql_dlt = "DELETE FROM `" . $syslogdb_default . "`. $from_table - WHERE message LIKE '" . $remove['message'] . "%' "; - } + $column = $column_map[$remove['type']] ?? 'message'; + $filter = syslog_build_match_filter($remove['type'], $remove['message'], $column); - } elseif ($remove['type'] == 'messagec') { - if ($remove['method'] != 'del') { - $sql_sel = "SELECT seq FROM `" . $syslogdb_default . "`. $from_table - WHERE message LIKE '%" . $remove['message'] . "%' "; - } else { - $sql_dlt = "DELETE FROM `" . $syslogdb_default . "`. $from_table - WHERE message LIKE '%" . $remove['message'] . "%' "; - } - } elseif ($remove['type'] == 'messagee') { - if ($remove['method'] != 'del') { - $sql_sel = "SELECT seq FROM `" . $syslogdb_default . "`. $from_table - WHERE message LIKE '%" . $remove['message'] . "' "; - } else { - $sql_dlt = "DELETE FROM `" . $syslogdb_default . "`. $from_table - WHERE message LIKE '%" . $remove['message'] . "' "; - } - } elseif ($remove['type'] == 'sql') { - if ($remove['method'] != 'del') { - $sql_sel = "SELECT seq FROM `" . $syslogdb_default . "`. $from_table - WHERE message (" . $remove['message'] . ") "; - } else { - $sql_dlt = "DELETE FROM `" . $syslogdb_default . "`. $from_table - WHERE message (" . $remove['message'] . ") "; - } + if ($remove['method'] != 'del') { + $sql_sel = "SELECT seq FROM `" . $syslogdb_default . "`. $from_table WHERE " . $filter['sql']; + } else { + $sql_dlt = "DELETE FROM `" . $syslogdb_default . "`. $from_table WHERE " . $filter['sql']; } + $params = $filter['params']; if ($sql_sel != '' || $sql_dlt != '') { $debugm = ''; @@ -980,7 +922,7 @@ function syslog_manage_items($from_table, $to_table) { if ($sql_sel != '') { $move_count = 0; /* first insert, then delete */ - $move_records = syslog_db_fetch_assoc($sql_sel); + $move_records = syslog_db_fetch_assoc_prepared($sql_sel, $params); syslog_debug(sprintf('Found %5s - Message(s)', cacti_sizeof($move_records))); if (cacti_sizeof($move_records)) { @@ -991,7 +933,7 @@ function syslog_manage_items($from_table, $to_table) { } $all_seq = preg_replace('/^,/i', '', $all_seq); - syslog_db_execute("INSERT INTO `". $syslogdb_default . "`.`". $to_table ."` + syslog_db_execute_prepared("INSERT INTO `". $syslogdb_default . "`.`". $to_table ."` (facility_id, priority_id, host_id, logtime, message) (SELECT facility_id, priority_id, host_id, logtime, message FROM `". $syslogdb_default . "`.". $from_table ." @@ -1000,7 +942,7 @@ function syslog_manage_items($from_table, $to_table) { $messages_moved = db_affected_rows($syslog_cnn); if ($messages_moved > 0) { - syslog_db_execute("DELETE FROM `". $syslogdb_default . "`.`" . $from_table ."` + syslog_db_execute_prepared("DELETE FROM `". $syslogdb_default . "`.`" . $from_table ."` WHERE seq IN (" . $all_seq .")" ); } @@ -1013,7 +955,7 @@ function syslog_manage_items($from_table, $to_table) { if ($sql_dlt != '') { /* now delete the remainder that match */ - syslog_db_execute($sql_dlt); + syslog_db_execute_prepared($sql_dlt, $params); $removed += db_affected_rows($syslog_cnn); $debugm = sprintf('Deleted %5s Message(s)', $removed); } @@ -1070,93 +1012,6 @@ function syslog_array2xml($array, $tag = 'template') { return $xml; } -/** - * syslog_execute_ticket_command - run the configured ticketing command for an alert - * - * @param array $alert The alert row from syslog_alert table - * @param array $hostlist Hostnames matched by the alert - * @param string $error_message sprintf template used if exec() returns non-zero - * - * @return void - */ -function syslog_execute_ticket_command($alert, $hostlist, $error_message) { - $command = read_config_option('syslog_ticket_command'); - - if ($command != '') { - $command = trim($command); - } - - if ($alert['open_ticket'] == 'on' && $command != '') { - /* trim surrounding quotes so paths like "/usr/bin/cmd" resolve correctly */ - $cparts = preg_split('/\s+/', trim($command)); - $executable = trim($cparts[0], '"\''); - - if (cacti_sizeof($cparts) && is_executable($executable)) { - $command = $command . - ' --alert-name=' . cacti_escapeshellarg(clean_up_name($alert['name'])) . - ' --severity=' . cacti_escapeshellarg($alert['severity']) . - ' --hostlist=' . cacti_escapeshellarg(implode(',', $hostlist)) . - ' --message=' . cacti_escapeshellarg($alert['message']); - - $output = array(); - $return = 0; - - exec($command, $output, $return); - - if ($return !== 0) { - cacti_log(sprintf($error_message, $alert['name'], $return, implode(', ', $output)), false, 'SYSLOG'); - } - } else { - $reason = (strpos($executable, DIRECTORY_SEPARATOR) === false) - ? 'PATH-based lookups are not supported; use an absolute path' - : 'file not found or not marked executable'; - cacti_log("SYSLOG ERROR: Ticket command is not executable: '$command' -- $reason", false, 'SYSTEM'); - } - } -} - -/** - * syslog_execute_alert_command - run the per-alert shell command for a matched result - * - * @param array $alert The alert row from syslog_alert table - * @param array $results The matched syslog result row - * @param string $hostname Resolved hostname for the source device - * - * @return void - */ -function syslog_execute_alert_command($alert, $results, $hostname) { - /* alert_replace_variables() escapes each substituted token (, - * , , , , ) with - * cacti_escapeshellarg(). The command template itself comes from admin - * configuration ($alert['command']) and is trusted at that boundary. - * Do not introduce additional substitution paths that bypass this escaping. */ - $command = alert_replace_variables($alert, $results, $hostname); - - /* trim surrounding quotes so paths like "/usr/bin/cmd" resolve correctly */ - $cparts = preg_split('/\s+/', trim($command)); - $executable = trim($cparts[0], '"\''); - - $output = array(); - $return = 0; - - if (cacti_sizeof($cparts) && is_executable($executable)) { - exec($command, $output, $return); - - if ($return !== 0 && !empty($output)) { - cacti_log('SYSLOG NOTICE: Alert command output: ' . implode(', ', $output), true, 'SYSTEM'); - } - - if ($return !== 0) { - cacti_log(sprintf('ERROR: Alert command failed. Alert:%s, Exit:%s, Output:%s', $alert['name'], $return, implode(', ', $output)), false, 'SYSLOG'); - } - } else { - $reason = (strpos($executable, DIRECTORY_SEPARATOR) === false) - ? 'PATH-based lookups are not supported; use an absolute path' - : 'file not found or not marked executable'; - cacti_log("SYSLOG ERROR: Alert command is not executable: '$command' -- $reason", false, 'SYSTEM'); - } -} - /** * syslog_process_alerts - Process each of the Syslog Alerts * @@ -1173,11 +1028,11 @@ function syslog_execute_alert_command($alert, $results, $hostname) { * and more importantly, to be able to have a separate re-alert cycles for that very same message as there can be similar messages * happening all the time at the system level, so it's hard to target a single host for re-alert rules. * - * @param (int) The max_seq to process + * @param (int) The unique id to process * * @return (array) An array of the number of alerts processed and the number of alerts generated */ -function syslog_process_alerts($max_seq) { +function syslog_process_alerts($uniqueID) { global $syslogdb_default; $syslog_alarms = 0; @@ -1201,6 +1056,7 @@ function syslog_process_alerts($max_seq) { if (cacti_sizeof($alerts)) { foreach($alerts as $alert) { $sql = ''; + $th_sql = ''; $params = array(); /* we roll up statistics depending on the level */ @@ -1210,7 +1066,7 @@ function syslog_process_alerts($max_seq) { $groupBy = ''; } - $sql_data = syslog_get_alert_sql($alert, $max_seq); + $sql_data = syslog_get_alert_sql($alert, $uniqueID); if (!cacti_sizeof($sql_data)) { syslog_debug(sprintf('Error - Unable to determine SQL for Alert \'%s\'', $alert['name'])); @@ -1220,8 +1076,18 @@ function syslog_process_alerts($max_seq) { $sql = $sql_data['sql']; $params = $sql_data['params']; + /** + * For this next step in processing, we want to call the syslog_process_alert + * once for every host, or system level breach that is encountered. This removes + * must of the complexity that would otherwise go into the syslog_process_alert + * function. + */ if ($sql != '') { if ($alert['level'] == '1') { + /** + * This is a host level alert process each host separately + * both thresholed and system levels have the same process + */ $th_sql = str_replace('*', 'host, COUNT(*) AS count', $sql); $results = syslog_db_fetch_assoc_prepared($th_sql . $groupBy, $params); @@ -1236,10 +1102,16 @@ function syslog_process_alerts($max_seq) { } } } elseif ($alert['method'] == '1') { + /** + * This is a system level threshold breach + */ $th_sql = str_replace('*', 'COUNT(*)', $sql); $count = syslog_db_fetch_cell_prepared($th_sql . $groupBy, $params); $syslog_alarms += syslog_process_alert($alert, $sql, $params, $count); } else { + /** + * This is a system level classic syslog breach without a threshold + */ $count = 0; $syslog_alarms += syslog_process_alert($alert, $sql, $params, $count); } @@ -1250,6 +1122,112 @@ function syslog_process_alerts($max_seq) { return array('syslog_alerts' => $syslog_alerts, 'syslog_alarms' => $syslog_alarms); } +function syslog_execute_ticket_command($alert, $hostlist, $context) { + if ($alert['open_ticket'] == 'on') { + $command = trim(read_config_option('syslog_ticket_command')); + + if ($command != '') { + /* + * Extract the executable portion from the configured command. + * This allows for quoted paths and additional arguments in the + * configuration while still validating the executable itself. + */ + $executable = $command; + $firstChar = substr($executable, 0, 1); + + if ($firstChar === '"' || $firstChar === "'") { + $quoteChar = $firstChar; + $closing = strpos($executable, $quoteChar, 1); + + if ($closing !== false) { + $executable = substr($executable, 1, $closing - 1); + } else { + // Unbalanced quotes; fall back to trimming quotes/whitespace. + $executable = trim($executable, " \t\n\r\0\x0B\"'"); + } + } else { + $parts = preg_split('/\s+/', $executable); + if (is_array($parts) && isset($parts[0])) { + $executable = $parts[0]; + } + $executable = trim($executable, " \t\n\r\0\x0B\"'"); + } + + if ($executable !== '' && is_executable($executable)) { + $command = $command . + ' --alert-name=' . cacti_escapeshellarg(clean_up_name($alert['name'])) . + ' --severity=' . cacti_escapeshellarg($alert['severity']) . + ' --hostlist=' . cacti_escapeshellarg(implode(',',$hostlist)) . + ' --message=' . cacti_escapeshellarg($alert['message']); + + $output = array(); + $return = 0; + + exec($command, $output, $return); + + if ($return != 0) { + cacti_log(sprintf('ERROR: %s Failed. Alert:%s, Exit:%s, Output:%s', $context, $alert['name'], $return, implode(', ', $output)), false, 'SYSLOG'); + } + } else { + if (strpos($executable, DIRECTORY_SEPARATOR) === false) { + cacti_log(sprintf('SYSLOG ERROR: %s path \'%s\' is missing absolute path separator', $context, $executable), false, 'SYSTEM'); + } else { + cacti_log(sprintf('SYSLOG ERROR: %s path \'%s\' is not executable', $context, $executable), false, 'SYSTEM'); + } + } + } + } +} + +function syslog_execute_alert_command($alert, $results, $hostname) { + $command = trim($alert['command']); + + if ($command != '') { + $command = alert_replace_variables($alert, $results, $hostname); + + /* + * Extract the executable portion from the command string. + * This allows for quoted paths and additional arguments. + */ + $executable = $command; + $firstChar = substr($executable, 0, 1); + + if ($firstChar === '"' || $firstChar === "'") { + $quoteChar = $firstChar; + $closing = strpos($executable, $quoteChar, 1); + + if ($closing !== false) { + $executable = substr($executable, 1, $closing - 1); + } else { + // Unbalanced quotes; fall back to trimming quotes/whitespace. + $executable = trim($executable, " \t\n\r\0\x0B\"'"); + } + } else { + $parts = preg_split('/\s+/', $executable); + if (is_array($parts) && isset($parts[0])) { + $executable = $parts[0]; + } + $executable = trim($executable, " \t\n\r\0\x0B\"'"); + } + + if ($executable !== '' && is_executable($executable)) { + $output = array(); + $returnCode = 0; + + exec($command, $output, $returnCode); + + $logMessage = "SYSLOG NOTICE: Executing '$command' Command return code: $returnCode"; + cacti_log($logMessage, true, 'SYSTEM'); + } else { + if (strpos($executable, DIRECTORY_SEPARATOR) === false) { + cacti_log(sprintf('SYSLOG ERROR: Alert Command path \'%s\' is missing absolute path separator', $executable), false, 'SYSTEM'); + } else { + cacti_log(sprintf('SYSLOG ERROR: Alert Command path \'%s\' is not executable', $executable), false, 'SYSTEM'); + } + } + } +} + /** * syslog_process_alert - Process the Alert and generate notifications, execute commands, etc. * @@ -1553,9 +1531,9 @@ function syslog_process_alert($alert, $sql, $params, $count, $hostname = '') { /** * Open a ticket if this options have been selected. */ - syslog_execute_ticket_command($alert, $hostlist, 'ERROR: Ticket Command Failed. Alert:%s, Exit:%s, Output:%s'); + syslog_execute_ticket_command($alert, $hostlist, 'Ticket Command'); - if (trim($alert['command']) != '' && !$found) { + if (!$found) { syslog_execute_alert_command($alert, $results, $hostname); } @@ -1574,9 +1552,12 @@ function syslog_process_alert($alert, $sql, $params, $count, $hostname = '') { alert_setup_environment($alert, $results, $hostlist, $hostname); - syslog_execute_ticket_command($alert, $hostlist, 'ERROR: Command Failed. Alert:%s, Exit:%s, Output:%s'); + /** + * Open a ticket if this options have been selected. + */ + syslog_execute_ticket_command($alert, $hostlist, 'Command'); - if (trim($alert['command']) != '' && !$found) { + if (!$found) { syslog_execute_alert_command($alert, $results, $hostname); } } @@ -1597,7 +1578,70 @@ function syslog_process_alert($alert, $sql, $params, $count, $hostname = '') { * * @return (array) The SQL and the prepared array for the SQL */ -function syslog_get_alert_sql(&$alert, $max_seq) { +/** + * syslog_build_match_filter - Builds a SQL WHERE clause and parameters for common match types. + * + * @param (string) $type The match type (facility, host, program, messageb, messagec, messagee, sql) + * @param (string) $value The search value + * @param (string) $column The column name to search against (default depends on type) + * + * @return (array) Array with 'sql' and 'params' keys + */ +function syslog_build_match_filter($type, $value, $column = '') { + global $syslogdb_default; + $sql = ''; + $params = array(); + + switch ($type) { + case 'facility': + if ($column == 'facility_id') { + $sql = 'facility_id IN (SELECT DISTINCT facility_id FROM `' . $syslogdb_default . '`.`syslog_facilities` WHERE facility = ?)'; + } else { + $sql = ($column ?: 'facility') . ' = ?'; + } + $params[] = $value; + break; + case 'host': + if ($column == 'host_id') { + $sql = 'host_id IN (SELECT DISTINCT host_id FROM `' . $syslogdb_default . '`.`syslog_hosts` WHERE host = ?)'; + } else { + $sql = ($column ?: 'host') . ' = ?'; + } + $params[] = $value; + break; + case 'program': + if ($column == 'program_id') { + $sql = 'program_id IN (SELECT DISTINCT program_id FROM `' . $syslogdb_default . '`.`syslog_programs` WHERE program = ?)'; + } else { + $sql = ($column ?: 'program') . ' = ?'; + } + $params[] = $value; + break; + case 'messageb': + $sql = ($column ?: 'message') . ' LIKE ?'; + $params[] = $value . '%'; + break; + case 'messagec': + $sql = ($column ?: 'message') . ' LIKE ?'; + $params[] = '%' . $value . '%'; + break; + case 'messagee': + $sql = ($column ?: 'message') . ' LIKE ?'; + $params[] = '%' . $value; + break; + case 'sql': + /* The 'sql' match type passes admin-configured expressions directly into + * the WHERE clause. This is an intentional trust boundary: only Cacti + * administrators with console access can configure removal/alert rules. + * No programmatic sanitization can safely parse arbitrary SQL fragments. */ + $sql = '(' . $value . ')'; + break; + } + + return array('sql' => $sql, 'params' => $params); +} + +function syslog_get_alert_sql(&$alert, $uniqueID) { global $syslogdb_default, $syslog_incoming_config; if (defined('SYSLOG_CONFIG')) { @@ -1608,79 +1652,32 @@ function syslog_get_alert_sql(&$alert, $max_seq) { $syslog_incoming_config['programField'] = 'program'; } - $params = array(); - $sql = ''; + $field_map = array( + 'facility' => $syslog_incoming_config['facilityField'], + 'host' => $syslog_incoming_config['hostField'], + 'program' => $syslog_incoming_config['programField'], + 'messageb' => $syslog_incoming_config['textField'], + 'messagec' => $syslog_incoming_config['textField'], + 'messagee' => $syslog_incoming_config['textField'], + ); - if ($alert['type'] == 'facility') { - $sql = 'SELECT * - FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `' . $syslog_incoming_config['facilityField'] . '` = ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = $alert['message']; - $params[] = $max_seq; - } elseif ($alert['type'] == 'messageb') { - $sql = 'SELECT * - FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `' . $syslog_incoming_config['textField'] . '` LIKE ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = $alert['message'] . '%'; - $params[] = $max_seq; - } elseif ($alert['type'] == 'messagec') { - $sql = 'SELECT * - FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `' . $syslog_incoming_config['textField'] . '` LIKE ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = '%' . $alert['message'] . '%'; - $params[] = $max_seq; - } elseif ($alert['type'] == 'messagee') { - $sql = 'SELECT * - FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `' . $syslog_incoming_config['textField'] . '` LIKE ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = '%' . $alert['message']; - $params[] = $max_seq; - } elseif ($alert['type'] == 'host') { - $sql = 'SELECT * - FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `' . $syslog_incoming_config['hostField'] . '` = ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = $alert['message']; - $params[] = $max_seq; - } elseif ($alert['type'] == 'program') { - $sql = 'SELECT * - FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `' . $syslog_incoming_config['programField'] . '` = ? - AND `status` = 1 - AND `seq` <= ?'; - - $params[] = $alert['message']; - $params[] = $max_seq; - } elseif ($alert['type'] == 'sql') { - $sql = 'SELECT * - FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE (' . $alert['message'] . ') - AND `status` = 1 - AND `seq` <= ?'; + $column = $field_map[$alert['type']] ?? ''; + $filter = syslog_build_match_filter($alert['type'], $alert['message'], $column); - $params[] = $max_seq; - } + $sql = 'SELECT * + FROM `' . $syslogdb_default . '`.`syslog_incoming` + WHERE ' . $filter['sql'] . ' + AND `status` = ?'; + + $params = $filter['params']; + $params[] = $uniqueID; return array('sql' => $sql, 'params' => $params); } /** - * syslog_preprocess_incoming_records - Generate a max_seq to allow moving of - * records to done table and mark incoming records with the max_seq and + * syslog_preprocess_incoming_records - Generate a uniqueID to allow moving of + * records to done table and mark incoming records with the uniqueID and * then if syslog is configured to strip domains, perform that first. * * @return (int) Unique id to allow syslog messages that come in randomly to @@ -1688,49 +1685,78 @@ function syslog_get_alert_sql(&$alert, $max_seq) { * to be left till then ext polling cycle. */ function syslog_preprocess_incoming_records() { - global $syslogdb_default; + global $syslogdb_default, $syslog_cnn; - $max_seq = syslog_db_fetch_cell('SELECT MAX(seq) FROM `' . $syslogdb_default . '`.`syslog_incoming` WHERE status = 0'); + $lock_name = hash('sha256', $syslogdb_default . '.preprocess_incoming'); + $locked = syslog_db_fetch_cell_prepared('SELECT GET_LOCK(?, 10)', array($lock_name)); - if ($max_seq > 0) { - /* flag all records with the status = 1 prior to moving */ - syslog_db_execute_prepared('UPDATE `' . $syslogdb_default . '`.`syslog_incoming` - SET `status` = 1 - WHERE `status` = 0 - AND `seq` <= ?', - array($max_seq)); + if ((int)$locked !== 1) { + cacti_log('SYSLOG ERROR: Unable to acquire preprocess lock', false, 'SYSTEM'); + return array('uniqueID' => 0, 'incoming' => 0); + } - syslog_debug('Max Sequence ID = ' . $max_seq); - syslog_debug('-------------------------------------------------------------------------------------'); + $uniqueID = 0; + $incoming = 0; - $syslog_incoming = syslog_db_fetch_cell_prepared('SELECT COUNT(seq) - FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `status` = 1 - AND `seq` <= ?', - array($max_seq)); + /* uniqueID is constrained to tinyint range (1-127) by the status column. + * Collision probability rises with concurrent pollers; the retry loop + * (up to 256 attempts) mitigates but does not eliminate this risk. + * A future schema change to widen status to int would allow a larger space. */ + try { + $attempts = 0; + while (1) { + $uniqueID = rand(1, 127); - syslog_debug(sprintf('Found %5s - New Message(s) to process', $syslog_incoming)); + $count = syslog_db_fetch_cell_prepared('SELECT COUNT(*) + FROM `' . $syslogdb_default . '`.`syslog_incoming` + WHERE `status` = ?', + array($uniqueID)); - /* strip domains if we have requested to do so */ - syslog_strip_incoming_domains($max_seq); + if ($count == 0) { + break; + } + + $attempts++; + + if ($attempts >= 256) { + cacti_log('SYSLOG ERROR: Unable to find unused uniqueID after 256 attempts', false, 'SYSTEM'); + return array('uniqueID' => 0, 'incoming' => 0); + } + } - api_plugin_hook('plugin_syslog_before_processing'); + /* flag all records with the uniqueID prior to moving */ + syslog_db_execute_prepared('UPDATE `' . $syslogdb_default . '`.`syslog_incoming` + SET `status` = ? + WHERE `status` = 0', + array($uniqueID)); - return array('max_seq' => $max_seq, 'incoming' => $syslog_incoming); + $incoming = db_affected_rows($syslog_cnn); + } finally { + syslog_db_fetch_cell_prepared('SELECT RELEASE_LOCK(?)', array($lock_name)); } - return array('max_seq' => 0, 'incoming' => 0); + syslog_debug('Unique ID = ' . $uniqueID); + syslog_debug('-------------------------------------------------------------------------------------'); + + syslog_debug(sprintf('Found %5s - New Message(s) to process', $incoming)); + + /* strip domains if we have requested to do so */ + syslog_strip_incoming_domains($uniqueID); + + api_plugin_hook('plugin_syslog_before_processing'); + + return array('uniqueID' => $uniqueID, 'incoming' => $incoming); } /** * syslog_strip_incoming_domains - If syslog is setup to strip DNS domain name suffixes do that * prior to processing the records. * - * @param (string) The max_seq records to process + * @param (string) The uniqueID records to process * * @return (void) */ -function syslog_strip_incoming_domains($max_seq) { +function syslog_strip_incoming_domains($uniqueID) { global $syslogdb_default; $syslog_domains = read_config_option('syslog_domains'); @@ -1739,28 +1765,29 @@ function syslog_strip_incoming_domains($max_seq) { $domains = explode(',', trim($syslog_domains)); foreach($domains as $domain) { - syslog_db_execute_prepared('UPDATE `' . $syslogdb_default . '`.`syslog_incoming` - SET host = SUBSTRING_INDEX(host, \'.\', 1) + syslog_db_execute_prepared('UPDATE `' . $syslogdb_default . "`.`syslog_incoming` + SET host = SUBSTRING_INDEX(host, '.', 1) WHERE host LIKE ? - AND `status` = 1 - AND `seq` <= ?', - array('%' . $domain, $max_seq)); + AND `status` = ?", + array('%' . $domain, $uniqueID)); } } } + + /** * Check if the hostname is in the cacti hosts table * Some devices only send IP addresses in syslog messages, and may not be in the DNS * however they may be in the cacti hosts table as monitored devices. * * @param (string) The hostname to check - * @param (int) The max_seq for syslog_incoming messages to process + * @param (int) The unique id for syslog_incoming messages to process * * @return (bool) True if the host exists in the Cacti database, false otherwise */ -function syslog_check_cacti_hosts($host, $max_seq) { +function syslog_check_cacti_hosts($host, $uniqueID) { global $syslogdb_default; if (empty($host)) { @@ -1778,9 +1805,8 @@ function syslog_check_cacti_hosts($host, $max_seq) { syslog_db_execute_prepared('UPDATE `' . $syslogdb_default . '`.`syslog_incoming` SET host = ? WHERE host = ? - AND `status` = 1 - AND `seq` <= ?', - array($cacti_host['description'], $host, $max_seq)); + AND `status` = ?', + array($cacti_host['description'], $host, $uniqueID)); return true; } @@ -1796,11 +1822,11 @@ function syslog_check_cacti_hosts($host, $max_seq) { * and assign an id to each of them. This way the syslog table can be optimized * for size as much as possible. * - * @param (int) The max_seq for syslog_incoming messages to process + * @param (int) The unique id for syslog_incoming messages to process * * @return (void) */ -function syslog_update_reference_tables($max_seq) { +function syslog_update_reference_tables($uniqueID) { global $syslogdb_default; syslog_debug('-------------------------------------------------------------------------------------'); @@ -1810,9 +1836,8 @@ function syslog_update_reference_tables($max_seq) { if (read_config_option('syslog_resolve_hostname') == 'on') { $hosts = syslog_db_fetch_assoc_prepared('SELECT DISTINCT host FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `status` = 1 - AND `seq` <= ?', - array($max_seq)); + WHERE `status` = ?', + array($uniqueID)); foreach($hosts as $host) { if (!isset($host['host']) || empty($host['host'])) { @@ -1831,7 +1856,7 @@ function syslog_update_reference_tables($max_seq) { // Check if hostname exists in Cacti hosts table (only if not already resolved via DNS) if (!$resolved) { - $resolved = syslog_check_cacti_hosts($host['host'], $max_seq); + $resolved = syslog_check_cacti_hosts($host['host'], $uniqueID); } // If not resolved via DNS or found in Cacti, prefix the hostname @@ -1841,9 +1866,8 @@ function syslog_update_reference_tables($max_seq) { syslog_db_execute_prepared('UPDATE `' . $syslogdb_default . "`.`syslog_incoming` SET host = ? WHERE host = ? - AND `status` = 1 - AND `seq` <= ?", - array($unresolved_host, $host['host'], $max_seq)); + AND `status` = ?", + array($unresolved_host, $host['host'], $uniqueID)); } } } @@ -1852,23 +1876,21 @@ function syslog_update_reference_tables($max_seq) { (program, last_updated) SELECT DISTINCT program, NOW() FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `status` = 1 - AND `seq` <= ? + WHERE `status` = ? ON DUPLICATE KEY UPDATE program=VALUES(program), last_updated=VALUES(last_updated)', - array($max_seq)); + array($uniqueID)); syslog_db_execute_prepared('INSERT INTO `' . $syslogdb_default . '`.`syslog_hosts` (host, last_updated) SELECT DISTINCT host, NOW() AS last_updated FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `status` = 1 - AND `seq` <= ? + WHERE `status` = ? ON DUPLICATE KEY UPDATE host=VALUES(host), last_updated=NOW()', - array($max_seq)); + array($uniqueID)); syslog_db_execute_prepared('INSERT INTO `' . $syslogdb_default . '`.`syslog_host_facilities` (host_id, facility_id) @@ -1877,8 +1899,7 @@ function syslog_update_reference_tables($max_seq) { ( SELECT DISTINCT host, facility_id FROM `' . $syslogdb_default . "`.`syslog_incoming` - WHERE `status` = 1 - AND `seq` <= ? + WHERE `status` = ? ) AS s INNER JOIN `" . $syslogdb_default . '`.`syslog_hosts` AS sh ON s.host = sh.host @@ -1886,18 +1907,18 @@ function syslog_update_reference_tables($max_seq) { ON DUPLICATE KEY UPDATE host_id=VALUES(host_id), last_updated=NOW()', - array($max_seq)); + array($uniqueID)); } /** * syslog_update_statistics - Insert new statistics rows into the syslog statistics * table for post review * - * @param (int) The max_seq for all syslog incoming records to be processed + * @param (int) The unique id for all syslog incoming records to be processed * * @return (void) */ -function syslog_update_statistics($max_seq) { +function syslog_update_statistics($uniqueID) { global $syslogdb_default, $syslog_cnn; if (read_config_option('syslog_statistics') == 'on') { @@ -1910,11 +1931,10 @@ function syslog_update_statistics($max_seq) { ON sh.host=si.host INNER JOIN syslog_programs AS sp ON sp.program=si.program - WHERE si.`status` = 1 - AND si.`seq` <= ? + WHERE `status` = ? GROUP BY host_id, priority_id, facility_id, program_id) AS merge GROUP BY host_id, priority_id, facility_id, program_id', - array($max_seq)); + array($uniqueID)); $stats = db_affected_rows($syslog_cnn); @@ -1929,11 +1949,11 @@ function syslog_update_statistics($max_seq) { * the syslog table, and then after which we can perform various * removal rules against them. * - * @param (int) The max_seq for rows in the syslog table + * @param (int) The unique id for rows in the syslog table * * @return (int) The number of rows moved to the syslog table */ -function syslog_incoming_to_syslog($max_seq) { +function syslog_incoming_to_syslog($uniqueID) { global $syslogdb_default, $syslog_cnn; syslog_db_execute_prepared('INSERT INTO `' . $syslogdb_default . '`.`syslog` @@ -1946,10 +1966,9 @@ function syslog_incoming_to_syslog($max_seq) { ON sh.host = si.host INNER JOIN syslog_programs AS sp ON sp.program = si.program - WHERE si.`status` = 1 - AND si.`seq` <= ? + WHERE `status` = ? ) AS merge', - array($max_seq)); + array($uniqueID)); $moved = db_affected_rows($syslog_cnn); @@ -1958,14 +1977,11 @@ function syslog_incoming_to_syslog($max_seq) { syslog_debug(sprintf('Moved %5s - Message(s) to the syslog table', $moved)); - syslog_db_execute_prepared('DELETE FROM `' . $syslogdb_default . '`.`syslog_incoming` - WHERE `status` = 1 - AND `seq` <= ?', - array($max_seq)); + syslog_db_execute_prepared('DELETE FROM `' . $syslogdb_default . '`.`syslog_incoming` WHERE status = ?', array($uniqueID)); syslog_debug(sprintf('Deleted %5s - Already Processed Message(s) from incoming', db_affected_rows($syslog_cnn))); - syslog_db_execute('DELETE FROM `' . $syslogdb_default . '`.`syslog_incoming` WHERE logtime < DATE_SUB(NOW(), INTERVAL 1 HOUR)'); + syslog_db_execute_prepared('DELETE FROM `' . $syslogdb_default . '`.`syslog_incoming` WHERE logtime < DATE_SUB(NOW(), INTERVAL 1 HOUR)'); $stale = db_affected_rows($syslog_cnn); @@ -1999,7 +2015,7 @@ function syslog_postprocess_tables() { syslog_debug(sprintf('Deleted %5s - Syslog Statistics Record(s)', db_affected_rows($syslog_cnn))); } } else { - syslog_db_execute('TRUNCATE `' . $syslogdb_default . '`.`syslog_statistics`'); + syslog_db_execute_prepared('TRUNCATE `' . $syslogdb_default . '`.`syslog_statistics`'); } /* remove alert log messages */ @@ -2035,14 +2051,14 @@ function syslog_postprocess_tables() { if (date('G') == 0 && date('i') < 5) { syslog_debug('Optimizing Tables'); if (!syslog_is_partitioned()) { - syslog_db_execute('OPTIMIZE TABLE + syslog_db_execute_prepared('OPTIMIZE TABLE `' . $syslogdb_default . '`.`syslog_incoming`, `' . $syslogdb_default . '`.`syslog`, `' . $syslogdb_default . '`.`syslog_remove`, `' . $syslogdb_default . '`.`syslog_removed`, `' . $syslogdb_default . '`.`syslog_alert`'); } else { - syslog_db_execute('OPTIMIZE TABLE + syslog_db_execute_prepared('OPTIMIZE TABLE `' . $syslogdb_default . '`.`syslog_incoming`, `' . $syslogdb_default . '`.`syslog_remove`, `' . $syslogdb_default . '`.`syslog_alert`'); @@ -2077,6 +2093,18 @@ function syslog_process_reports() { $html = false; } + $from_email = read_config_option('settings_from_email'); + if ($from_email == '') { + $from_email = 'Cacti@cacti.net'; + } + + $from_name = read_config_option('settings_from_name'); + if ($from_name == '') { + $from_name = 'Cacti Reporting'; + } + + $from = array($from_email, $from_name); + /* Lets run the reports */ $reports = syslog_db_fetch_assoc('SELECT * FROM `' . $syslogdb_default . "`.`syslog_reports` @@ -2130,16 +2158,18 @@ function syslog_process_reports() { $reptext = ''; - $sql = syslog_get_report_sql($report); + $report_sql = syslog_get_report_sql($report); - if ($sql != '') { + if ($report_sql['sql'] != '') { $date2 = date('Y-m-d H:i:s', $current_time); $date1 = date('Y-m-d H:i:s', $current_time - $time_span); - $sql .= " AND logtime BETWEEN ". db_qstr($date1) . " AND " . db_qstr($date2); - $sql .= ' ORDER BY logtime DESC'; - $items = syslog_db_fetch_assoc($sql); + + $sql = $report_sql['sql'] . ' AND logtime BETWEEN ? AND ? ORDER BY logtime DESC'; + $params = array_merge($report_sql['params'], array($date1, $date2)); + + $items = syslog_db_fetch_assoc_prepared($sql, $params); - syslog_debug('We have ' . db_affected_rows($syslog_cnn) . ' items for the Report'); + syslog_debug('We have ' . cacti_sizeof($items) . ' items for the Report'); $classes = array('even', 'odd'); @@ -2215,66 +2245,60 @@ function syslog_process_reports() { * * @param (array) The report to process * - * @return (string) The unprepared SQL + * @return (array) The SQL and the prepared array for the SQL */ function syslog_get_report_sql(&$report) { global $syslogdb_default; - if ($report['type'] == 'messageb') { - $sql = 'SELECT sl.*, sh.host - FROM `' . $syslogdb_default . '`.`syslog` AS sl - INNER JOIN `' . $syslogdb_default . '`.`syslog_hosts` AS sh - ON sl.host_id = sh.host_id - WHERE message LIKE ' . db_qstr($report['message'] . '%'); - } - - if ($report['type'] == 'messagec') { - $sql = 'SELECT sl.*, sh.host - FROM `' . $syslogdb_default . '`.`syslog` AS sl - INNER JOIN `' . $syslogdb_default . '`.`syslog_hosts` AS sh - ON sl.host_id = sh.host_id - WHERE message LIKE ' . db_qstr('%' . $report['message'] . '%'); - } - - if ($report['type'] == 'messagee') { - $sql = 'SELECT sl.*, sh.host - FROM `' . $syslogdb_default . '`.`syslog` AS sl - INNER JOIN `' . $syslogdb_default . '`.`syslog_hosts` AS sh - ON sl.host_id = sh.host_id - WHERE message LIKE ' . db_qstr('%' . $report['message']); - } - - if ($report['type'] == 'host') { - $sql = 'SELECT sl.*, sh.host - FROM `' . $syslogdb_default . '`.`syslog` AS sl - INNER JOIN `' . $syslogdb_default . '`.`syslog_hosts` AS sh - ON sl.host_id = sh.host_id - WHERE sh.host = ' . db_qstr($report['message']); - } - - if ($report['type'] == 'facility') { - $sql = 'SELECT sl.*, sf.facility - FROM `' . $syslogdb_default . '`.`syslog` AS sl - INNER JOIN `' . $syslogdb_default . '`.`syslog_facilities` AS sf - ON sl.facility_id = sf.facility_id - WHERE sf.facility = ' . db_qstr($report['message']); - } + $sql = ''; + $params = array(); - if ($report['type'] == 'program') { - $sql = 'SELECT sl.*, sp.program - FROM `' . $syslogdb_default . '`.`syslog` AS sl - INNER JOIN `' . $syslogdb_default . '`.`syslog_programs` AS sp - ON sl.program_id = sp.program_id - WHERE sp.program = ' . db_qstr($report['message']); - } + $column_map = array( + 'facility' => 'sf.facility', + 'host' => 'sh.host', + 'program' => 'sp.program', + 'messageb' => 'message', + 'messagec' => 'message', + 'messagee' => 'message', + ); - if ($report['type'] == 'sql') { - $sql = 'SELECT * - FROM `' . $syslogdb_default . '`.`syslog` - WHERE (' . $report['message'] . ')'; + $column = $column_map[$report['type']] ?? ''; + $filter = syslog_build_match_filter($report['type'], $report['message'], $column); + + if (!empty($filter['sql'])) { + if ($report['type'] == 'host') { + $sql = 'SELECT sl.*, sh.host + FROM `' . $syslogdb_default . '`.`syslog` AS sl + INNER JOIN `' . $syslogdb_default . '`.`syslog_hosts` AS sh + ON sl.host_id = sh.host_id + WHERE ' . $filter['sql']; + } elseif ($report['type'] == 'facility') { + $sql = 'SELECT sl.*, sf.facility + FROM `' . $syslogdb_default . '`.`syslog` AS sl + INNER JOIN `' . $syslogdb_default . '`.`syslog_facilities` AS sf + ON sl.facility_id = sf.facility_id + WHERE ' . $filter['sql']; + } elseif ($report['type'] == 'program') { + $sql = 'SELECT sl.*, sp.program + FROM `' . $syslogdb_default . '`.`syslog` AS sl + INNER JOIN `' . $syslogdb_default . '`.`syslog_programs` AS sp + ON sl.program_id = sp.program_id + WHERE ' . $filter['sql']; + } elseif ($report['type'] == 'sql') { + $sql = 'SELECT * + FROM `' . $syslogdb_default . '`.`syslog` + WHERE ' . $filter['sql']; + } else { + $sql = 'SELECT sl.*, sh.host + FROM `' . $syslogdb_default . '`.`syslog` AS sl + INNER JOIN `' . $syslogdb_default . '`.`syslog_hosts` AS sh + ON sl.host_id = sh.host_id + WHERE ' . $filter['sql']; + } + $params = $filter['params']; } - return $sql; + return array('sql' => $sql, 'params' => $params); } /** diff --git a/js/functions.js b/js/functions.js index a96d47d..fe3a219 100644 --- a/js/functions.js +++ b/js/functions.js @@ -11,7 +11,7 @@ * Clear filter for statistics view */ function clearFilterStats() { - strURL = 'syslog.php?tab=stats&clear=1&header=false'; + var strURL = 'syslog.php?tab=stats&clear=1&header=false'; loadPageNoHeader(strURL); } @@ -196,7 +196,7 @@ function initSyslogMain(config) { $(this).multiselect('widget').find(':checkbox:first').each(function() { $(this).prop('checked', true); }); - $('#test').trigger('keyup'); + $('#term').trigger('keyup'); }, checkAll: function() { $(this).multiselect('widget').find(':checkbox').not(':first').each(function() { @@ -220,7 +220,11 @@ function initSyslogMain(config) { $.each(data, function(index, hostData) { if ($('#host option[value="'+index+'"]').length == 0) { - $('#host').append(''); + var option = $(''; + print html_escape($host['host']) . ''; } } ?> @@ -1734,27 +1734,27 @@ function syslog_messages($tab = 'syslog') { $url .= ""; } - form_selectable_cell($url, $sm['seq'], '', 'left'); + form_selectable_ecell($url, $sm['seq'], '', 'left'); } // Display grouped or individual messages if ($grouping_enabled && isset($sm['occurrence_count']) && $sm['occurrence_count'] > 1) { // Grouped message display with expand/collapse $expand_icon = ""; - form_selectable_cell($expand_icon . $sm['logtime'], $sm['seq'], '', 'left'); + form_selectable_ecell($expand_icon . $sm['logtime'], $sm['seq'], '', 'left'); } else { - form_selectable_cell($sm['logtime'], $sm['seq'], '', 'left'); + form_selectable_ecell($sm['logtime'], $sm['seq'], '', 'left'); } - form_selectable_cell(isset($hosts[$sm['host_id']]) ? $hosts[$sm['host_id']]:__('Unknown', 'syslog'), $sm['seq'], '', 'left'); - form_selectable_cell($sm['program'], $sm['seq'], '', 'left'); - form_selectable_cell(filter_value(title_trim($sm[$syslog_incoming_config['textField']], get_request_var_request('trimval')), get_request_var('rfilter')), $sm['seq'], '', 'left syslogMessage'); - form_selectable_cell(isset($facilities[$sm['facility_id']]) ? $facilities[$sm['facility_id']]:__('Unknown', 'syslog'), $sm['seq'], '', 'left'); - form_selectable_cell(isset($priorities[$sm['priority_id']]) ? $priorities[$sm['priority_id']]:__('Unknown', 'syslog'), $sm['seq'], '', 'left'); + form_selectable_ecell(isset($hosts[$sm['host_id']]) ? $hosts[$sm['host_id']]:__('Unknown', 'syslog'), $sm['seq'], '', 'left'); + form_selectable_ecell($sm['program'], $sm['seq'], '', 'left'); + form_selectable_ecell(filter_value(title_trim($sm[$syslog_incoming_config['textField']], get_request_var_request('trimval')), get_request_var('rfilter')), $sm['seq'], '', 'left syslogMessage'); + form_selectable_ecell(isset($facilities[$sm['facility_id']]) ? $facilities[$sm['facility_id']]:__('Unknown', 'syslog'), $sm['seq'], '', 'left'); + form_selectable_ecell(isset($priorities[$sm['priority_id']]) ? $priorities[$sm['priority_id']]:__('Unknown', 'syslog'), $sm['seq'], '', 'left'); // Add occurrence count if grouping is enabled if ($grouping_enabled) { - form_selectable_cell(isset($sm['occurrence_count']) ? $sm['occurrence_count'] : 1, $sm['seq'], '', 'right'); + form_selectable_ecell(isset($sm['occurrence_count']) ? $sm['occurrence_count'] : 1, $sm['seq'], '', 'right'); } form_end_row(); @@ -1843,16 +1843,16 @@ function syslog_messages($tab = 'syslog') { syslog_log_row_color($log['severity'], $title); - form_selectable_cell(filter_value($log['name'] != '' ? $log['name']:__('Alert Removed', 'syslog'), get_request_var('rfilter'), $config['url_path'] . 'plugins/syslog/syslog.php?id=' . $log['seq'] . '&tab=current'), $log['seq'], '', 'left'); + form_selectable_ecell(filter_value($log['name'] != '' ? $log['name']:__('Alert Removed', 'syslog'), get_request_var('rfilter'), $config['url_path'] . 'plugins/syslog/syslog.php?id=' . $log['seq'] . '&tab=current'), $log['seq'], '', 'left'); - form_selectable_cell(isset($severities[$log['severity']]) ? $severities[$log['severity']]:__('Unknown', 'syslog'), $log['seq'], '', 'left'); - form_selectable_cell($log['logtime'], $log['seq'], '', 'left'); - form_selectable_cell(filter_value(title_trim($log['logmsg'], get_request_var_request('trimval')), get_request_var('rfilter')), $log['seq'], '', 'syslogMessage left'); + form_selectable_ecell(isset($severities[$log['severity']]) ? $severities[$log['severity']]:__('Unknown', 'syslog'), $log['seq'], '', 'left'); + form_selectable_ecell($log['logtime'], $log['seq'], '', 'left'); + form_selectable_ecell(filter_value(title_trim($log['logmsg'], get_request_var_request('trimval')), get_request_var('rfilter')), $log['seq'], '', 'syslogMessage left'); - form_selectable_cell($log['count'], $log['seq'], '', 'right'); - form_selectable_cell($log['host'], $log['seq'], '', 'right'); - form_selectable_cell(ucfirst($log['facility']), $log['seq'], '', 'right'); - form_selectable_cell(ucfirst($log['priority']), $log['seq'], '', 'right'); + form_selectable_ecell($log['count'], $log['seq'], '', 'right'); + form_selectable_ecell($log['host'], $log['seq'], '', 'right'); + form_selectable_ecell(ucfirst($log['facility']), $log['seq'], '', 'right'); + form_selectable_ecell(ucfirst($log['priority']), $log['seq'], '', 'right'); form_end_row(); } @@ -2055,4 +2055,3 @@ function syslog_form_callback($form_name, $classic_sql, $column_display, $column - -

    " . __('Click \'Continue\' to Delete the following Syslog Alert Rule(s).', 'syslog') . "

    -
      $alert_list
    "; - print " - - \n"; - - $title = __esc('Delete Syslog Alert Rule(s)', 'syslog'); - } elseif (get_request_var('drp_action') == '2') { /* disable */ - print " - -

    " . __('Click \'Continue\' to Disable the following Syslog Alert Rule(s).', 'syslog') . "

    -
      $alert_list
    "; - print " - - \n"; - - $title = __esc('Disable Syslog Alert Rule(s)', 'syslog'); - } elseif (get_request_var('drp_action') == '3') { /* enable */ - print " - -

    " . __('Click \'Continue\' to Enable the following Syslog Alert Rule(s).', 'syslog') . "

    -
      $alert_list
    "; - print " - - \n"; - - $title = __esc('Enable Syslog Alert Rule(s)', 'syslog'); - } elseif (get_request_var('drp_action') == '4') { /* export */ - print " - -

    " . __('Click \'Continue\' to Export the following Syslog Alert Rule(s).', 'syslog') . "

    -
      $alert_list
    "; - print " - - \n"; - - $title = __esc('Export Syslog Alert Rule(s)', 'syslog'); - } + $action_verbs = array( + '1' => __('Delete', 'syslog'), + '2' => __('Disable', 'syslog'), + '3' => __('Enable', 'syslog'), + '4' => __('Export', 'syslog') + ); + + syslog_draw_bulk_action_confirm(get_request_var('drp_action'), $alert_list, __('Syslog Alert Rule(s)', 'syslog'), $action_verbs); + + $titles = array( + '1' => __('Delete Syslog Alert Rule(s)', 'syslog'), + '2' => __('Disable Syslog Alert Rule(s)', 'syslog'), + '3' => __('Enable Syslog Alert Rule(s)', 'syslog'), + '4' => __('Export Syslog Alert Rule(s)', 'syslog') + ); + + $title = $titles[get_request_var('drp_action')] ?? ''; $save_html = "  - -

    " . __('Click \'Continue\' to Delete the following Syslog Removal Rule(s).', 'syslog') . "

    -
      $removal_list
    "; - print " - - \n"; - - $title = __esc('Delete Syslog Removal Rule(s)', 'syslog'); - } else if (get_request_var('drp_action') == '2') { /* disable */ - print " - -

    " . __('Click \'Continue\' to Disable the following Syslog Removal Rule(s).', 'syslog') . "

    -
      $removal_list
    "; - print " - - \n"; - - $title = __esc('Disable Syslog Removal Rule(s)', 'syslog'); - } else if (get_request_var('drp_action') == '3') { /* enable */ - print " - -

    " . __('Click \'Continue\' to Enable the following Syslog Removal Rule(s).', 'syslog') . "

    -
      $removal_list
    "; - print " - - \n"; - - $title = __esc('Enable Syslog Removal Rule(s)', 'syslog'); - } else if (get_request_var('drp_action') == '4') { /* reprocess */ - print " - -

    " . __('Click \'Continue\' to Re-process the following Syslog Removal Rule(s).', 'syslog') . "

    -
      $removal_list
    "; - print " - - \n"; - - $title = __esc('Retroactively Process Syslog Removal Rule(s)', 'syslog'); - } elseif (get_request_var('drp_action') == '5') { /* export */ - print " - -

    " . __('Click \'Continue\' to Export the following Syslog Removal Rule(s).', 'syslog') . "

    -
      $removal_list
    "; - print " - - \n"; - - $title = __esc('Export Syslog Removal Rule(s)', 'syslog'); - } + $action_verbs = array( + '1' => __('Delete', 'syslog'), + '2' => __('Disable', 'syslog'), + '3' => __('Enable', 'syslog'), + '4' => __('Re-process', 'syslog'), + '5' => __('Export', 'syslog') + ); + + syslog_draw_bulk_action_confirm(get_request_var('drp_action'), $removal_list, __('Syslog Removal Rule(s)', 'syslog'), $action_verbs); + + $titles = array( + '1' => __('Delete Syslog Removal Rule(s)', 'syslog'), + '2' => __('Disable Syslog Removal Rule(s)', 'syslog'), + '3' => __('Enable Syslog Removal Rule(s)', 'syslog'), + '4' => __('Retroactively Process Syslog Removal Rule(s)', 'syslog'), + '5' => __('Export Syslog Removal Rule(s)', 'syslog') + ); + + $title = $titles[get_request_var('drp_action')] ?? ''; - $save_html = "  "; } else { + raise_message(40); header('Location: syslog_removal.php?header=false'); exit; @@ -654,13 +623,13 @@ function syslog_removal() { if (cacti_sizeof($removals)) { foreach ($removals as $removal) { form_alternate_row('line' . $removal['id'], true); - form_selectable_cell(filter_value(title_trim($removal['name'], read_config_option('max_title_length')), get_request_var('filter'), $config['url_path'] . 'plugins/syslog/syslog_removal.php?action=edit&id=' . $removal['id']), $removal['id']); - form_selectable_cell((($removal['enabled'] == 'on') ? __('Yes', 'syslog'):__('No', 'syslog')), $removal['id']); - form_selectable_cell($message_types[$removal['type']], $removal['id']); + form_selectable_ecell(filter_value(title_trim($removal['name'], read_config_option('max_title_length')), get_request_var('filter'), $config['url_path'] . 'plugins/syslog/syslog_removal.php?action=edit&id=' . $removal['id']), $removal['id']); + form_selectable_ecell((($removal['enabled'] == 'on') ? __('Yes', 'syslog'):__('No', 'syslog')), $removal['id']); + form_selectable_ecell($message_types[$removal['type']], $removal['id']); form_selectable_ecell($removal['message'], $removal['id']); - form_selectable_cell((($removal['method'] == 'del') ? __('Deletion', 'syslog'): __('Transfer', 'syslog')), $removal['id']); - form_selectable_cell(date('Y-m-d H:i:s', $removal['date']), $removal['id']); - form_selectable_cell($removal['user'], $removal['id']); + form_selectable_ecell((($removal['method'] == 'del') ? __('Deletion', 'syslog'): __('Transfer', 'syslog')), $removal['id']); + form_selectable_ecell(date('Y-m-d H:i:s', $removal['date']), $removal['id']); + form_selectable_ecell($removal['user'], $removal['id']); form_checkbox_cell($removal['name'], $removal['id']); form_end_row(); } @@ -731,20 +700,7 @@ function import() { } function removal_import() { - $import_text = get_nfilter_request_var('import_text'); - - if (trim($import_text) != '') { - /* textbox input */ - $xml_data = $import_text; - } elseif (($_FILES['import_file']['tmp_name'] != 'none') && ($_FILES['import_file']['tmp_name'] != '')) { - /* file upload */ - $fp = fopen($_FILES['import_file']['tmp_name'],'r'); - $xml_data = fread($fp, filesize($_FILES['import_file']['tmp_name'])); - fclose($fp); - } else { - header('Location: syslog_removal.php?header=false'); - exit; - } + $xml_data = syslog_get_import_xml_payload('syslog_removal.php?header=false'); /* obtain debug information if it's set */ $xml_array = xml2array($xml_data); diff --git a/syslog_reports.php b/syslog_reports.php index e7340aa..9e625a1 100644 --- a/syslog_reports.php +++ b/syslog_reports.php @@ -152,53 +152,29 @@ function form_actions() { FROM `' . $syslogdb_default . '`.`syslog_reports` WHERE id=' . $matches[1]); - $report_list .= '
  • ' . $report_info . '
  • '; + $report_list .= '
  • ' . html_escape($report_info) . '
  • '; $report_array[] = $matches[1]; } } if (cacti_sizeof($report_array)) { - if (get_request_var('drp_action') == '1') { /* delete */ - print " - -

    " . __('Click \'Continue\' to Delete the following Syslog Report(s).', 'syslog') . "

    -
    "; - print " - - \n"; - - $title = __esc('Delete Syslog Report(s)', 'syslog'); - } elseif (get_request_var('drp_action') == '2') { /* disable */ - print " - -

    " . __('Click \'Continue\' to Disable the following Syslog Report(s).', 'syslog') . "

    -
    "; - print " - - \n"; - - $title = __esc('Disable Syslog Report(s)', 'syslog'); - } elseif (get_request_var('drp_action') == '3') { /* enable */ - print " - -

    " . __('Click \'Continue\' to Enable the following Syslog Report(s).', 'syslog') . "

    -
    "; - print " - - \n"; - - $title = __esc('Enable Syslog Report(s)', 'syslog'); - } elseif (get_request_var('drp_action') == '4') { /* export */ - print " - -

    " . __('Click \'Continue\' to Export the following Syslog Report Rule(s).', 'syslog') . "

    -
    "; - print " - - \n"; - - $title = __esc('Export Syslog Report Rule(s)', 'syslog'); - } + $action_verbs = array( + '1' => __('Delete', 'syslog'), + '2' => __('Disable', 'syslog'), + '3' => __('Enable', 'syslog'), + '4' => __('Export', 'syslog') + ); + + syslog_draw_bulk_action_confirm(get_request_var('drp_action'), $report_list, __('Syslog Report(s)', 'syslog'), $action_verbs); + + $titles = array( + '1' => __('Delete Syslog Report(s)', 'syslog'), + '2' => __('Disable Syslog Report(s)', 'syslog'), + '3' => __('Enable Syslog Report(s)', 'syslog'), + '4' => __('Export Syslog Report(s)', 'syslog') + ); + + $title = $titles[get_request_var('drp_action')] ?? ''; $save_html = "  'fetch_row_prepared', 'sql' => $sql, 'params' => $params]; + return syslog_get_mock_result('fetch_row_prepared', array()); + } +} + +if (!function_exists('syslog_db_fetch_assoc_prepared')) { + function syslog_db_fetch_assoc_prepared($sql, $params = array(), $log = TRUE) { + $GLOBALS['syslog_db_calls'][] = ['method' => 'fetch_assoc_prepared', 'sql' => $sql, 'params' => $params]; + return syslog_get_mock_result('fetch_assoc_prepared', array()); + } +} + +if (!function_exists('syslog_db_fetch_cell_prepared')) { + function syslog_db_fetch_cell_prepared($sql, $params = array(), $log = TRUE) { + $GLOBALS['syslog_db_calls'][] = ['method' => 'fetch_cell_prepared', 'sql' => $sql, 'params' => $params]; + return syslog_get_mock_result('fetch_cell_prepared', ''); + } +} + +if (!function_exists('syslog_db_execute_prepared')) { + function syslog_db_execute_prepared($sql, $params = array(), $log = TRUE) { + $GLOBALS['syslog_db_calls'][] = ['method' => 'execute_prepared', 'sql' => $sql, 'params' => $params]; + return true; + } +} + +if (!function_exists('syslog_db_execute')) { + function syslog_db_execute($sql, $log = TRUE) { + $GLOBALS['syslog_db_calls'][] = ['method' => 'execute', 'sql' => $sql]; + return true; + } +} + +if (!function_exists('syslog_db_fetch_assoc')) { + function syslog_db_fetch_assoc($sql, $log = TRUE) { + $GLOBALS['syslog_db_calls'][] = ['method' => 'fetch_assoc', 'sql' => $sql]; + return syslog_get_mock_result('fetch_assoc', array()); + } +} + +if (!function_exists('db_fetch_row_prepared')) { + function db_fetch_row_prepared($sql, $params = array(), $log = TRUE) { + $GLOBALS['syslog_db_calls'][] = ['method' => 'db_fetch_row', 'sql' => $sql, 'params' => $params]; + return syslog_get_mock_result('db_fetch_row', array()); + } +} + +if (!function_exists('db_fetch_cell_prepared')) { + function db_fetch_cell_prepared($sql, $params = array(), $log = TRUE) { + $GLOBALS['syslog_db_calls'][] = ['method' => 'db_fetch_cell', 'sql' => $sql, 'params' => $params]; + return syslog_get_mock_result('db_fetch_cell', ''); + } +} + +if (!function_exists('db_fetch_assoc')) { + function db_fetch_assoc($sql, $log = TRUE) { + $GLOBALS['syslog_db_calls'][] = ['method' => 'db_fetch_assoc', 'sql' => $sql]; + return syslog_get_mock_result('db_fetch_assoc', array()); + } +} + +if (!function_exists('db_fetch_assoc_prepared')) { + function db_fetch_assoc_prepared($sql, $params = array(), $log = TRUE) { + $GLOBALS['syslog_db_calls'][] = ['method' => 'db_fetch_assoc_prepared', 'sql' => $sql, 'params' => $params]; + return syslog_get_mock_result('db_fetch_assoc_prepared', array()); + } +} + +if (!function_exists('db_qstr')) { + function db_qstr($string) { + return "'" . addslashes((string)$string) . "'"; + } +} + +if (!function_exists('html_escape')) { + function html_escape($string) { + return htmlspecialchars((string)$string, ENT_QUOTES, 'UTF-8'); + } +} + +if (!function_exists('html_escape_request_var')) { + function html_escape_request_var($name) { + return html_escape($_POST[$name] ?? $_GET[$name] ?? ''); + } +} + +if (!function_exists('api_plugin_hook_function')) { + function api_plugin_hook_function($name, $params = array()) { + $GLOBALS['syslog_hook_calls'][] = ['name' => $name, 'params' => $params]; + } +} + +if (!function_exists('__')) { + function __($str, $domain) { + return $str; + } +} + +if (!function_exists('__esc')) { + function __esc($str, $arg1 = null, $arg2 = null) { + if ($arg1 !== null) { + return sprintf($str, $arg1); + } + return $str; + } +} diff --git a/tests/Integration/SyslogCactiHostIntegrationTest.php b/tests/Integration/SyslogCactiHostIntegrationTest.php new file mode 100644 index 0000000..a5aa525 --- /dev/null +++ b/tests/Integration/SyslogCactiHostIntegrationTest.php @@ -0,0 +1,119 @@ +toBeFalse(); + expect($GLOBALS['syslog_db_calls'])->toBeEmpty(); + }); + + test('syslog_check_cacti_hosts() returns false when host not found in Cacti', function () { + $GLOBALS['syslog_db_results']['db_fetch_row'] = array(); + + $result = syslog_check_cacti_hosts('unknown-host', 12345); + + expect($result)->toBeFalse(); + expect($GLOBALS['syslog_db_calls'])->toHaveCount(1); + expect($GLOBALS['syslog_db_calls'][0]['method'])->toBe('db_fetch_row'); + expect($GLOBALS['syslog_db_calls'][0]['params'])->toBe(['unknown-host']); + }); + + test('syslog_check_cacti_hosts() returns true and updates when host is found in Cacti', function () { + $GLOBALS['syslog_db_results']['db_fetch_row'] = array('description' => 'Resolved Name'); + + $result = syslog_check_cacti_hosts('1.2.3.4', 12345); + + expect($result)->toBeTrue(); + expect($GLOBALS['syslog_db_calls'])->toHaveCount(2); + + // Check lookup + expect($GLOBALS['syslog_db_calls'][0]['method'])->toBe('db_fetch_row'); + expect($GLOBALS['syslog_db_calls'][0]['params'])->toBe(['1.2.3.4']); + + // Check update + expect($GLOBALS['syslog_db_calls'][1]['method'])->toBe('execute_prepared'); + expect($GLOBALS['syslog_db_calls'][1]['params'])->toBe(['Resolved Name', '1.2.3.4', 12345]); + expect($GLOBALS['syslog_db_calls'][1]['sql'])->toContain('UPDATE `cacti_syslog`.`syslog_incoming`'); + }); + + test('syslog_update_reference_tables() resolves hostnames via Cacti lookup', function () { + $uniqueID = 54321; + $GLOBALS['syslog_test_config']['syslog_resolve_hostname'] = 'on'; + $GLOBALS['syslog_test_config']['syslog_no_dns'] = 'on'; + + // Mock fetch of hosts from syslog_incoming. Need to wrap in extra array for sequence mock + $GLOBALS['syslog_db_results']['fetch_assoc_prepared'] = [ + [ array('host' => '10.0.0.1') ] + ]; + + // Mock Cacti host lookup for 10.0.0.1 + $GLOBALS['syslog_db_results']['db_fetch_row'] = [ array('description' => 'Server-01') ]; + + syslog_update_reference_tables($uniqueID); + + // Should have called: + // 1. fetch_assoc_prepared (get hosts) + // 2. db_fetch_row (syslog_check_cacti_hosts) + // 3. execute (update hostname in syslog_incoming) + // ... (other calls like syslog_programs, syslog_hosts) + + $foundLookup = false; + $foundUpdate = false; + + foreach ($GLOBALS['syslog_db_calls'] as $call) { + if ($call['method'] === 'db_fetch_row' && isset($call['params'][0]) && $call['params'][0] === '10.0.0.1') { + $foundLookup = true; + } + if ($call['method'] === 'execute_prepared' && strpos($call['sql'], 'UPDATE `cacti_syslog`.`syslog_incoming`') !== false && isset($call['params'][0]) && $call['params'][0] === 'Server-01') { + $foundUpdate = true; + } + } + + expect($foundLookup)->toBeTrue(); + expect($foundUpdate)->toBeTrue(); + }); + + test('syslog_update_reference_tables() marks hosts as unresolved if lookup fails', function () { + $uniqueID = 999; + $GLOBALS['syslog_test_config']['syslog_resolve_hostname'] = 'on'; + $GLOBALS['syslog_test_config']['syslog_no_dns'] = 'on'; + + // Mock fetch of hosts from syslog_incoming + $GLOBALS['syslog_db_results']['fetch_assoc_prepared'] = [ + [ array('host' => 'unresolvable.local') ] + ]; + + // Mock Cacti host lookup as empty (not found) + $GLOBALS['syslog_db_results']['db_fetch_row'] = [ array() ]; + + syslog_update_reference_tables($uniqueID); + + $foundUpdate = false; + foreach ($GLOBALS['syslog_db_calls'] as $call) { + if ($call['method'] === 'execute_prepared' && strpos($call['sql'], 'UPDATE `cacti_syslog`.`syslog_incoming`') !== false && isset($call['params'][0]) && $call['params'][0] === 'unresolved-unresolvable.local') { + $foundUpdate = true; + } + } + expect($foundUpdate)->toBeTrue(); + }); +}); diff --git a/tests/Integration/SyslogDomainIntegrationTest.php b/tests/Integration/SyslogDomainIntegrationTest.php new file mode 100644 index 0000000..3a51659 --- /dev/null +++ b/tests/Integration/SyslogDomainIntegrationTest.php @@ -0,0 +1,41 @@ +toHaveCount(2); + + expect($GLOBALS['syslog_db_calls'][0]['method'])->toBe('execute_prepared'); + expect($GLOBALS['syslog_db_calls'][0]['params'])->toBe(['%example.com', 12345]); + expect($GLOBALS['syslog_db_calls'][1]['params'])->toBe(['%test.org', 12345]); + + expect($GLOBALS['syslog_db_calls'][0]['sql'])->toContain('WHERE host LIKE ?'); + expect($GLOBALS['syslog_db_calls'][0]['sql'])->toContain('AND `status` = ?'); + }); +}); diff --git a/tests/Integration/SyslogPartitioningIntegrationTest.php b/tests/Integration/SyslogPartitioningIntegrationTest.php new file mode 100644 index 0000000..28c4be2 --- /dev/null +++ b/tests/Integration/SyslogPartitioningIntegrationTest.php @@ -0,0 +1,70 @@ +toBeTrue(); + expect(syslog_partition_table_allowed('syslog_removed'))->toBeTrue(); + + expect(syslog_partition_table_allowed('users'))->toBeFalse(); + expect(syslog_partition_table_allowed('syslog; DROP TABLE syslog'))->toBeFalse(); + expect(syslog_partition_table_allowed(''))->toBeFalse(); + }); + + test('syslog_partition_create() returns false for disallowed tables', function () { + // This should return early without doing anything + $result = syslog_partition_create('invalid_table'); + expect($result)->toBeFalse(); + expect($GLOBALS['syslog_db_calls'])->toBeEmpty(); + }); + + test('syslog_partition_create() uses prepared statements and locking', function () { + global $syslogdb_default; + $syslogdb_default = 'cacti_syslog'; + + // Mock exists check to return false (partition does not exist) + // Since GlobalStubs returns empty array, it will think it doesn't exist. + + // Mock GET_LOCK to return 1 + $GLOBALS['syslog_db_results']['fetch_cell_prepared'] = 1; + + syslog_partition_create('syslog'); + + // Should have called: + // 1. fetch_row_prepared (exists check) + // 2. fetch_cell_prepared (GET_LOCK) + // 3. fetch_row_prepared (double-checked locking re-check) + // 4. execute_prepared (ALTER TABLE) + // 5. fetch_cell_prepared (RELEASE_LOCK) + + expect($GLOBALS['syslog_db_calls'])->toHaveCount(5); + expect($GLOBALS['syslog_db_calls'][0]['method'])->toBe('fetch_row_prepared'); + expect($GLOBALS['syslog_db_calls'][1]['method'])->toBe('fetch_cell_prepared'); + expect($GLOBALS['syslog_db_calls'][1]['sql'])->toContain('GET_LOCK'); + + expect($GLOBALS['syslog_db_calls'][2]['method'])->toBe('fetch_row_prepared'); + expect($GLOBALS['syslog_db_calls'][3]['method'])->toBe('execute_prepared'); + expect($GLOBALS['syslog_db_calls'][3]['sql'])->toContain('ALTER TABLE'); + + expect($GLOBALS['syslog_db_calls'][4]['method'])->toBe('fetch_cell_prepared'); + expect($GLOBALS['syslog_db_calls'][4]['sql'])->toContain('RELEASE_LOCK'); + }); +}); diff --git a/tests/Integration/SyslogPostProcessIntegrationTest.php b/tests/Integration/SyslogPostProcessIntegrationTest.php new file mode 100644 index 0000000..0ad4ed1 --- /dev/null +++ b/tests/Integration/SyslogPostProcessIntegrationTest.php @@ -0,0 +1,63 @@ +toContain('syslog_statistics'); + expect($deleted_tables)->toContain('syslog_logs'); + expect($deleted_tables)->toContain('syslog_hosts'); + expect($deleted_tables)->toContain('syslog_programs'); + expect($deleted_tables)->toContain('syslog_host_facilities'); + }); + + test('syslog_postprocess_tables() triggers hooks', function () { + $GLOBALS['syslog_test_config']['syslog_retention'] = 30; + $GLOBALS['syslog_test_config']['syslog_alert_retention'] = 7; + + syslog_postprocess_tables(); + + expect($GLOBALS['syslog_hook_calls'])->toHaveCount(1); + expect($GLOBALS['syslog_hook_calls'][0]['name'])->toBe('syslog_delete_hostsalarm'); + }); +}); diff --git a/tests/Integration/SyslogRemovalIntegrationTest.php b/tests/Integration/SyslogRemovalIntegrationTest.php new file mode 100644 index 0000000..9cc8e2c --- /dev/null +++ b/tests/Integration/SyslogRemovalIntegrationTest.php @@ -0,0 +1,146 @@ + 'facility_id', + 'priorityField' => 'priority_id', + 'programField' => 'program', + 'textField' => 'message', + 'dateField' => 'logtime', + 'timeField' => 'logtime', + 'hostField' => 'host', + 'messageField' => 'message' + ]; + }); + + test('syslog_manage_items() parameterizes facility removal', function () { + // Mock removal rules + $GLOBALS['syslog_db_results']['fetch_assoc'] = [ + [ + array( + 'id' => 1, + 'enabled' => 'on', + 'type' => 'facility', + 'method' => 'del', + 'message' => "test' OR '1'='1" + ) + ] + ]; + + syslog_manage_items('syslog', 'syslog_removed'); + + // Check if any execute_prepared call contains the unescaped message + $foundUnsafe = false; + foreach ($GLOBALS['syslog_db_calls'] as $call) { + if ($call['method'] === 'execute_prepared') { + if (strpos($call['sql'], "test' OR '1'='1") !== false) { + $foundUnsafe = true; + } + } + } + + // It should NOT be unsafe. If it's unsafe, it means it's not parameterized. + // Currently it IS unsafe according to my reading of the code. + expect($foundUnsafe)->toBeFalse(); + }); + + test('syslog_manage_items() parameterizes host removal', function () { + $GLOBALS['syslog_db_results']['fetch_assoc'] = [ + [ + array( + 'id' => 2, + 'enabled' => 'on', + 'type' => 'host', + 'method' => 'del', + 'message' => "host' OR '1'='1" + ) + ] + ]; + + syslog_manage_items('syslog', 'syslog_removed'); + + $foundUnsafe = false; + foreach ($GLOBALS['syslog_db_calls'] as $call) { + if ($call['method'] === 'execute_prepared') { + if (strpos($call['sql'], "host' OR '1'='1") !== false) { + $foundUnsafe = true; + } + } + } + expect($foundUnsafe)->toBeFalse(); + }); + + test('syslog_manage_items() parameterizes message-based removal', function () { + $GLOBALS['syslog_db_results']['fetch_assoc'] = [ + [ + array( + 'id' => 3, + 'enabled' => 'on', + 'type' => 'messagec', + 'method' => 'del', + 'message' => "some'-- unsafe" + ) + ] + ]; + + syslog_manage_items('syslog', 'syslog_removed'); + + $foundUnsafe = false; + foreach ($GLOBALS['syslog_db_calls'] as $call) { + if ($call['method'] === 'execute_prepared') { + if (strpos($call['sql'], "some'-- unsafe") !== false) { + $foundUnsafe = true; + } + } + } + expect($foundUnsafe)->toBeFalse(); + }); + + test('syslog_remove_items() handles facility removal for syslog_incoming', function () { + $uniqueID = 111; + $GLOBALS['syslog_db_results']['fetch_assoc'] = [ + [ + array( + 'id' => 4, + 'name' => 'Test Rule', + 'enabled' => 'on', + 'type' => 'facility', + 'method' => 'del', + 'message' => 'local0' + ) + ] + ]; + + syslog_remove_items('syslog_incoming', $uniqueID); + + // Check for delete call + $foundDelete = false; + foreach ($GLOBALS['syslog_db_calls'] as $call) { + if ($call['method'] === 'execute_prepared' && strpos($call['sql'], 'DELETE') !== false) { + if (in_array('local0', $call['params']) && in_array(111, $call['params'])) { + $foundDelete = true; + } + } + } + expect($foundDelete)->toBeTrue(); + }); +}); diff --git a/tests/Integration/SyslogReportXssIntegrationTest.php b/tests/Integration/SyslogReportXssIntegrationTest.php new file mode 100644 index 0000000..120be09 --- /dev/null +++ b/tests/Integration/SyslogReportXssIntegrationTest.php @@ -0,0 +1,86 @@ + $to, + 'subject' => $subject, + 'message' => $html_message ?: $text_message + ]; + } +} + +// Load the logic +require_once __DIR__ . '/../../functions.php'; + +describe('Syslog Report XSS Integration', function () { + beforeEach(function () { + $GLOBALS['config'] = ['base_path' => __DIR__]; + $GLOBALS['syslog_db_calls'] = []; + $GLOBALS['syslog_db_results'] = []; + $GLOBALS['syslog_test_config'] = []; + $GLOBALS['syslog_test_config']['cron_interval'] = 300; + $GLOBALS['syslog_sent_emails'] = []; + $GLOBALS['syslogdb_default'] = 'cacti_syslog'; + $GLOBALS['forcer'] = true; // Force report to run + }); + + test('syslog_process_reports() escapes host and message in email content', function () { + // Mock reports fetch + $GLOBALS['syslog_db_results']['fetch_assoc'] = [ + [ + [ + 'id' => 1, + 'name' => 'XSS Report', + 'enabled' => 'on', + 'type' => 'messagec', + 'message' => 'test', + 'lastsent' => 0, + 'timespan' => 3600, + 'timepart' => 0, + 'email' => 'test@example.com', + 'body' => 'Check this' + ] + ] + ]; + + // Mock items fetch (now uses fetch_assoc_prepared) + $GLOBALS['syslog_db_results']['fetch_assoc_prepared'] = [ + [ + [ + 'host' => '', + 'priority_id' => 1, + 'facility_id' => 1, + 'message' => '">', + 'logtime' => '2026-03-23 10:00:00' + ] + ] + ]; + + syslog_process_reports(); + + expect($GLOBALS['syslog_sent_emails'])->toHaveCount(1); + $email = $GLOBALS['syslog_sent_emails'][0]['message']; + + // Host should be escaped + expect($email)->toContain('<script>alert("host")</script>'); + expect($email)->not->toContain(''); + + // Message should be escaped + expect($email)->toContain('"><img src=x onerror=alert(1)>'); + expect($email)->not->toContain('">'); + }); +}); diff --git a/tests/Integration/lib/reports.php b/tests/Integration/lib/reports.php new file mode 100644 index 0000000..e69de29 diff --git a/tests/Integration/plugins/syslog/syslog.css b/tests/Integration/plugins/syslog/syslog.css new file mode 100644 index 0000000..e69de29 diff --git a/tests/Unit/SyslogImportPayloadTest.php b/tests/Unit/SyslogImportPayloadTest.php new file mode 100644 index 0000000..c2542ff --- /dev/null +++ b/tests/Unit/SyslogImportPayloadTest.php @@ -0,0 +1,21 @@ +test'; + $payload = syslog_get_import_xml_payload('http://localhost'); + expect($payload)->toBe('test'); +}); diff --git a/tests/Unit/SyslogMatchFilterTest.php b/tests/Unit/SyslogMatchFilterTest.php new file mode 100644 index 0000000..6a36f10 --- /dev/null +++ b/tests/Unit/SyslogMatchFilterTest.php @@ -0,0 +1,196 @@ + string, 'params' => array] and is the + * central WHERE-clause builder used by removal, alert, and report code + * paths. These tests cover every match type, the sql trust boundary, + * edge inputs, and special characters. + */ + +require_once __DIR__ . '/../Helpers/GlobalStubs.php'; +require_once __DIR__ . '/../../functions.php'; + +beforeEach(function (): void { + $GLOBALS['syslogdb_default'] = 'cacti_syslog'; +}); + +describe('syslog_build_match_filter — string equality types', function (): void { + test('facility type: plain column uses equality placeholder', function (): void { + $r = syslog_build_match_filter('facility', 'local0'); + expect($r['sql'])->toBe('facility = ?'); + expect($r['params'])->toBe(['local0']); + }); + + test('facility type: facility_id column uses subquery', function (): void { + $r = syslog_build_match_filter('facility', 'local0', 'facility_id'); + expect($r['sql'])->toContain('facility_id IN (SELECT DISTINCT facility_id FROM'); + expect($r['sql'])->toContain('syslog_facilities'); + expect($r['sql'])->toContain('WHERE facility = ?'); + expect($r['params'])->toBe(['local0']); + }); + + test('host type: plain column uses equality placeholder', function (): void { + $r = syslog_build_match_filter('host', '10.0.0.1'); + expect($r['sql'])->toBe('host = ?'); + expect($r['params'])->toBe(['10.0.0.1']); + }); + + test('host type: host_id column uses subquery', function (): void { + $r = syslog_build_match_filter('host', '10.0.0.1', 'host_id'); + expect($r['sql'])->toContain('host_id IN (SELECT DISTINCT host_id FROM'); + expect($r['sql'])->toContain('syslog_hosts'); + expect($r['sql'])->toContain('WHERE host = ?'); + expect($r['params'])->toBe(['10.0.0.1']); + }); + + test('program type: plain column uses equality placeholder', function (): void { + $r = syslog_build_match_filter('program', 'sshd'); + expect($r['sql'])->toBe('program = ?'); + expect($r['params'])->toBe(['sshd']); + }); + + test('program type: program_id column uses subquery', function (): void { + $r = syslog_build_match_filter('program', 'sshd', 'program_id'); + expect($r['sql'])->toContain('program_id IN (SELECT DISTINCT program_id FROM'); + expect($r['sql'])->toContain('syslog_programs'); + expect($r['sql'])->toContain('WHERE program = ?'); + expect($r['params'])->toBe(['sshd']); + }); +}); + +describe('syslog_build_match_filter — substring match types', function (): void { + test('messageb (starts with): appends trailing wildcard', function (): void { + $r = syslog_build_match_filter('messageb', 'kernel:', 'message'); + expect($r['sql'])->toBe('message LIKE ?'); + expect($r['params'])->toBe(['kernel:%']); + }); + + test('messagec (contains): wraps value in wildcards', function (): void { + $r = syslog_build_match_filter('messagec', 'error', 'message'); + expect($r['sql'])->toBe('message LIKE ?'); + expect($r['params'])->toBe(['%error%']); + }); + + test('messagee (ends with): prepends leading wildcard', function (): void { + $r = syslog_build_match_filter('messagee', 'failed', 'message'); + expect($r['sql'])->toBe('message LIKE ?'); + expect($r['params'])->toBe(['%failed']); + }); + + test('messageb uses default column when none supplied', function (): void { + $r = syslog_build_match_filter('messageb', 'start'); + expect($r['sql'])->toBe('message LIKE ?'); + }); + + test('messagec uses explicit column override', function (): void { + $r = syslog_build_match_filter('messagec', 'needle', 'logmessage'); + expect($r['sql'])->toBe('logmessage LIKE ?'); + expect($r['params'])->toBe(['%needle%']); + }); + + test('messagee uses explicit column override', function (): void { + $r = syslog_build_match_filter('messagee', 'tail', 'logtext'); + expect($r['sql'])->toBe('logtext LIKE ?'); + expect($r['params'])->toBe(['%tail']); + }); +}); + +describe('syslog_build_match_filter — sql trust boundary', function (): void { + /* + * The 'sql' type is intentional: only Cacti administrators with console + * access configure removal/alert rules, so arbitrary expressions are + * permitted. The contract is that the raw value is wrapped in parens and + * no params are bound. + */ + + test('sql type: wraps raw expression in parens, no bound params', function (): void { + $r = syslog_build_match_filter('sql', 'host_id = 42 AND facility_id < 10'); + expect($r['sql'])->toBe('(host_id = 42 AND facility_id < 10)'); + expect($r['params'])->toBe([]); + }); + + test('sql type: passes value through without escaping', function (): void { + $expr = "message LIKE '%critical%'"; + $r = syslog_build_match_filter('sql', $expr); + expect($r['sql'])->toBe('(' . $expr . ')'); + expect($r['params'])->toBe([]); + }); + + test('sql type: empty expression wraps empty parens', function (): void { + $r = syslog_build_match_filter('sql', ''); + expect($r['sql'])->toBe('()'); + expect($r['params'])->toBe([]); + }); +}); + +describe('syslog_build_match_filter — empty and null-like values', function (): void { + test('messageb with empty value produces trailing-wildcard-only param', function (): void { + $r = syslog_build_match_filter('messageb', ''); + expect($r['params'])->toBe(['%']); + }); + + test('messagec with empty value produces double-wildcard param', function (): void { + $r = syslog_build_match_filter('messagec', ''); + expect($r['params'])->toBe(['%%']); + }); + + test('messagee with empty value produces leading-wildcard-only param', function (): void { + $r = syslog_build_match_filter('messagee', ''); + expect($r['params'])->toBe(['%']); + }); + + test('host type with empty value still binds placeholder', function (): void { + $r = syslog_build_match_filter('host', ''); + expect($r['sql'])->toBe('host = ?'); + expect($r['params'])->toBe(['']); + }); + + test('unknown type returns empty sql and empty params', function (): void { + $r = syslog_build_match_filter('nonexistent', 'value'); + expect($r['sql'])->toBe(''); + expect($r['params'])->toBe([]); + }); +}); + +describe('syslog_build_match_filter — special characters in match values', function (): void { + test('SQL metacharacters in messagec value are passed as literal param (not interpolated)', function (): void { + $payload = "' OR '1'='1"; + $r = syslog_build_match_filter('messagec', $payload); + // Value must appear verbatim in the param, not in the SQL fragment. + expect($r['sql'])->not->toContain($payload); + expect($r['params'][0])->toBe('%' . $payload . '%'); + }); + + test('backslash in messageb value is passed to param intact', function (): void { + $r = syslog_build_match_filter('messageb', 'C:\\Windows\\'); + expect($r['params'][0])->toBe('C:\\Windows\\%'); + }); + + test('percent sign in messagec value is preserved in param', function (): void { + $r = syslog_build_match_filter('messagec', '50% done'); + expect($r['params'][0])->toBe('%50% done%'); + }); + + test('underscore in messagee value is preserved in param', function (): void { + $r = syslog_build_match_filter('messagee', 'auth_log'); + expect($r['params'][0])->toBe('%auth_log'); + }); + + test('host type with injection attempt binds raw value as param', function (): void { + $payload = "1 OR 1=1--"; + $r = syslog_build_match_filter('host', $payload); + expect($r['sql'])->toBe('host = ?'); + expect($r['params'])->toBe([$payload]); + }); + + test('subquery path: facility_id with special characters binds raw value as param', function (): void { + $payload = "'; DROP TABLE syslog;--"; + $r = syslog_build_match_filter('facility', $payload, 'facility_id'); + expect($r['sql'])->toContain('WHERE facility = ?'); + expect($r['params'])->toBe([$payload]); + }); +}); diff --git a/tests/Unit/SyslogPartitionTest.php b/tests/Unit/SyslogPartitionTest.php new file mode 100644 index 0000000..2db9c38 --- /dev/null +++ b/tests/Unit/SyslogPartitionTest.php @@ -0,0 +1,276 @@ +toBeTrue(); + }); + + test('accepts "syslog_removed"', function (): void { + expect(syslog_partition_table_allowed('syslog_removed'))->toBeTrue(); + }); + + test('rejects an arbitrary table name', function (): void { + expect(syslog_partition_table_allowed('host'))->toBeFalse(); + }); + + test('rejects an empty string', function (): void { + expect(syslog_partition_table_allowed(''))->toBeFalse(); + }); + + test('rejects a name with SQL injection characters', function (): void { + expect(syslog_partition_table_allowed('syslog; DROP TABLE syslog;--'))->toBeFalse(); + }); + + test('rejects a name with path traversal characters', function (): void { + expect(syslog_partition_table_allowed('../../../etc/passwd'))->toBeFalse(); + }); + + test('rejects a name with backtick injection', function (): void { + expect(syslog_partition_table_allowed('`syslog`'))->toBeFalse(); + }); + + test('rejects uppercase variant (case-sensitive allowlist)', function (): void { + expect(syslog_partition_table_allowed('SYSLOG'))->toBeFalse(); + expect(syslog_partition_table_allowed('Syslog'))->toBeFalse(); + }); + + test('rejects a partial match that is not in the allowlist', function (): void { + expect(syslog_partition_table_allowed('syslog_'))->toBeFalse(); + expect(syslog_partition_table_allowed('syslog_remove'))->toBeFalse(); + }); + + test('allowlist is strict: only exactly two values pass', function (): void { + $candidates = [ + 'syslog' => true, + 'syslog_removed' => true, + 'syslog_data' => false, + 'syslog2' => false, + 'plugin_syslog' => false, + '' => false, + ' syslog' => false, + 'syslog ' => false, + ]; + + foreach ($candidates as $name => $expected) { + expect(syslog_partition_table_allowed($name)) + ->toBe($expected, "Expected syslog_partition_table_allowed('$name') === " . ($expected ? 'true' : 'false')); + } + }); +}); + +describe('syslog_partition_table_allowed — regex defense-in-depth', function () use ($_functions_src): void { + /* + * Even for tables that pass the allowlist, a secondary regex ensures the + * identifier contains only [a-z_] characters. This is belt-and-suspenders + * against future changes that might widen the allowlist carelessly. + */ + + test('source contains [a-z_] regex guard inside syslog_partition_table_allowed', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_table_allowed\s*\(\s*\$table\s*\)\s*\{(.+?)^\}/ms', + $_functions_src, + $m + ); + $body = $m[1] ?? ''; + expect($body)->toContain('[a-z_]'); + }); + + test('function explicitly returns false for non-members', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_table_allowed\s*\(\s*\$table\s*\)\s*\{(.+?)^\}/ms', + $_functions_src, + $m + ); + $body = $m[1] ?? ''; + expect($body)->toContain('return false'); + }); +}); + +describe('syslog_partition_create — lock acquisition and release', function () use ($_functions_src): void { + test('acquires GET_LOCK before issuing ALTER TABLE', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_create\s*\(\s*\$table\s*\)\s*\{(.+?)^\}/ms', + $_functions_src, + $m + ); + $body = $m[1] ?? ''; + expect($body)->toContain('GET_LOCK'); + expect($body)->toContain('ALTER TABLE'); + // GET_LOCK position must precede ALTER TABLE. + expect(strpos($body, 'GET_LOCK'))->toBeLessThan(strpos($body, 'ALTER TABLE')); + }); + + test('releases lock in a finally block', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_create\s*\(\s*\$table\s*\)\s*\{(.+?)^\}/ms', + $_functions_src, + $m + ); + $body = $m[1] ?? ''; + expect($body)->toMatch('/finally\s*\{[^}]*RELEASE_LOCK/s'); + }); + + test('returns false immediately when allowlist rejects the table', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_create\s*\(\s*\$table\s*\)\s*\{(.{0,400})/s', + $_functions_src, + $m + ); + $head = $m[1] ?? ''; + expect($head)->toMatch('/!syslog_partition_table_allowed[^}]*return\s+false/s'); + }); + + test('lock name includes both syslogdb_default and table', function () use ($_functions_src): void { + expect($_functions_src)->toContain('syslog_partition_create.\' . $table'); + }); + + test('lock timeout is 10 seconds', function () use ($_functions_src): void { + // Verify the timeout value passed to GET_LOCK matches the 10s convention. + expect($_functions_src)->toContain('SELECT GET_LOCK(?, 10)'); + }); + + test('failed lock acquisition returns false without issuing DDL', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_create\s*\(\s*\$table\s*\)\s*\{(.+?)^\}/ms', + $_functions_src, + $m + ); + $body = $m[1] ?? ''; + // The guard on (int)$locked !== 1 must appear before ALTER TABLE. + $lockGuardPos = strpos($body, '(int)$locked !== 1'); + $alterTablePos = strpos($body, 'ALTER TABLE'); + expect($lockGuardPos)->not->toBeFalse(); + expect($alterTablePos)->not->toBeFalse(); + expect($lockGuardPos)->toBeLessThan($alterTablePos); + }); + + test('DDL safety comment documents why parameters are not bound', function () use ($_functions_src): void { + expect($_functions_src)->toContain('MySQL does not support parameter binding for DDL'); + }); + + test('partition format values are validated by regex before DDL', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_create\s*\(\s*\$table\s*\)\s*\{(.+?)^\}/ms', + $_functions_src, + $m + ); + $body = $m[1] ?? ''; + // Both $cformat and $lnow must pass preg_match validation. + expect($body)->toContain('preg_match'); + expect($body)->toContain('d\d{8}'); + }); +}); + +describe('syslog_partition_remove — lock acquisition and release', function () use ($_functions_src): void { + test('acquires GET_LOCK before issuing ALTER TABLE', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_remove\s*\(\s*\$table\s*\)\s*\{(.+?)^\}/ms', + $_functions_src, + $m + ); + $body = $m[1] ?? ''; + expect($body)->toContain('GET_LOCK'); + }); + + test('releases lock in a finally block', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_remove\s*\(\s*\$table\s*\)\s*\{(.+?)^\}/ms', + $_functions_src, + $m + ); + $body = $m[1] ?? ''; + expect($body)->toMatch('/finally\s*\{[^}]*RELEASE_LOCK/s'); + }); + + test('returns 0 immediately when allowlist rejects the table', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_remove\s*\(\s*\$table\s*\)\s*\{(.{0,400})/s', + $_functions_src, + $m + ); + $head = $m[1] ?? ''; + expect($head)->toMatch('/!syslog_partition_table_allowed[^}]*return\s+0/s'); + }); + + test('logs an error when called with a disallowed table', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_remove\s*\(\s*\$table\s*\)\s*\{(.{0,600})/s', + $_functions_src, + $m + ); + $head = $m[1] ?? ''; + expect($head)->toMatch('/!syslog_partition_table_allowed.*cacti_log.*disallowed/s'); + }); + + test('lock name is scoped per-operation and per-table', function () use ($_functions_src): void { + expect($_functions_src)->toContain('syslog_partition_remove.\' . $table'); + }); + + test('information_schema query uses prepared statement with table_name placeholder', function () use ($_functions_src): void { + preg_match( + '/function syslog_partition_remove\s*\(\s*\$table\s*\)\s*\{(.+?)^\}/ms', + $_functions_src, + $m + ); + $body = $m[1] ?? ''; + expect($body)->toMatch('/syslog_db_fetch_(?:row|assoc|cell)_prepared[^)]*information_schema[^)]*table_name\s*=\s*\?/s'); + }); +}); + +describe('syslog_partition_create — runtime allowlist rejection (pure PHP)', function (): void { + /* + * These call the live function against disallowed inputs to confirm the + * guard executes without touching any DB layer. + */ + + test('returns false for a disallowed table without touching DB', function (): void { + // syslog_partition_create returns false when the table is not in the + // allowlist, before any DB call is made. + $result = syslog_partition_create('evil_table'); + expect($result)->toBeFalse(); + }); + + test('returns false for empty table name', function (): void { + $result = syslog_partition_create(''); + expect($result)->toBeFalse(); + }); +}); + +describe('syslog_partition_remove — runtime allowlist rejection (pure PHP)', function (): void { + test('returns 0 for a disallowed table without touching DB', function (): void { + $result = syslog_partition_remove('evil_table'); + expect($result)->toBe(0); + }); + + test('returns 0 for empty table name', function (): void { + $result = syslog_partition_remove(''); + expect($result)->toBe(0); + }); +}); diff --git a/tests/Unit/SyslogPreprocessTest.php b/tests/Unit/SyslogPreprocessTest.php new file mode 100644 index 0000000..752fa15 --- /dev/null +++ b/tests/Unit/SyslogPreprocessTest.php @@ -0,0 +1,176 @@ + 0, 'incoming' => 0] when it cannot acquire a lock or exhausts + * all retry attempts. + * + * Runtime execution is not possible without a live DB, so these tests use + * source-verification plus isolated logic mirrors that match the implementation. + */ + +$_functions_src = file_get_contents(__DIR__ . '/../../functions.php'); +if ($_functions_src === false) { + fwrite(STDERR, "Failed to read functions.php\n"); + exit(1); +} + +describe('syslog_preprocess_incoming_records — uniqueID allocation contract', function () use ($_functions_src): void { + /* + * The uniqueID range is a schema invariant: the `status` column is TINYINT + * UNSIGNED NOT NULL which stores values 0-127 (signed tinyint in practice). + * Zero is reserved for "unprocessed", so the allocation range is 1-127. + */ + + test('uniqueID is allocated via rand(1, 127)', function () use ($_functions_src): void { + expect($_functions_src)->toContain('rand(1, 127)'); + }); + + test('rand call does not use 0 as lower bound', function () use ($_functions_src): void { + // rand(0, 127) would allow status=0 which is the unprocessed sentinel. + expect($_functions_src)->not->toContain('rand(0, 127)'); + }); + + test('rand call does not exceed 127 as upper bound', function () use ($_functions_src): void { + // Values >127 cannot be stored in the tinyint status column. + expect($_functions_src)->not->toContain('rand(1, 128)'); + expect($_functions_src)->not->toContain('rand(1, 255)'); + expect($_functions_src)->not->toContain('rand(1, 256)'); + }); + + test('uniqueID range 1-127 produces only valid tinyint values', function (): void { + // Mirror the allocation logic to verify the range invariant. + for ($i = 0; $i < 200; $i++) { + $id = rand(1, 127); + expect($id)->toBeGreaterThanOrEqual(1); + expect($id)->toBeLessThanOrEqual(127); + } + }); +}); + +describe('syslog_preprocess_incoming_records — retry exhaustion', function () use ($_functions_src): void { + test('function returns uniqueID 0 after 256 failed attempts', function () use ($_functions_src): void { + // Verify the retry cap of 256 is present in the source. + expect($_functions_src)->toContain('$attempts >= 256'); + }); + + test('returns array with uniqueID key 0 on retry exhaustion', function () use ($_functions_src): void { + // The return value must include the 'uniqueID' key set to 0 so callers + // can detect the failure without a null-access risk. + expect($_functions_src)->toContain("return array('uniqueID' => 0, 'incoming' => 0)"); + }); + + test('retry exhaustion path logs an error via cacti_log', function () use ($_functions_src): void { + expect($_functions_src)->toContain('Unable to find unused uniqueID after 256 attempts'); + }); + + test('mirror: retry loop returns 0 once attempt cap is hit', function (): void { + // Mirror of the retry logic with an always-busy stub. + $attempts = 0; + $uniqueID = 0; + $max = 256; + + while (true) { + $candidate = rand(1, 127); + $count = 1; // stub: slot always occupied + + if ($count == 0) { + $uniqueID = $candidate; + break; + } + + $attempts++; + + if ($attempts >= $max) { + $uniqueID = 0; + break; + } + } + + expect($uniqueID)->toBe(0); + expect($attempts)->toBe($max); + }); + + test('mirror: retry loop succeeds on first free slot', function (): void { + // Stub that reports busy for the first two candidates then free. + $busy = [true, true, false]; + $attempts = 0; + $uniqueID = 0; + $idx = 0; + + while (true) { + $candidate = rand(1, 127); + $occupied = $busy[$idx] ?? false; + $idx++; + + if (!$occupied) { + $uniqueID = $candidate; + break; + } + + $attempts++; + + if ($attempts >= 256) { + $uniqueID = 0; + break; + } + } + + expect($uniqueID)->toBeGreaterThanOrEqual(1); + expect($uniqueID)->toBeLessThanOrEqual(127); + expect($attempts)->toBe(2); + }); +}); + +describe('syslog_preprocess_incoming_records — lock acquisition', function () use ($_functions_src): void { + test('acquires a GET_LOCK before allocation loop', function () use ($_functions_src): void { + expect($_functions_src)->toContain('SELECT GET_LOCK(?, 10)'); + }); + + test('releases lock via RELEASE_LOCK in a finally block', function () use ($_functions_src): void { + // Verify RELEASE_LOCK sits inside a finally so it runs even on exception. + expect($_functions_src)->toMatch('/finally\s*\{[^}]*RELEASE_LOCK/s'); + }); + + test('lock acquisition failure returns early with uniqueID 0', function () use ($_functions_src): void { + // When GET_LOCK returns !=1 the function must return immediately. + expect($_functions_src)->toContain("(int)\$locked !== 1"); + expect($_functions_src)->toContain("return array('uniqueID' => 0, 'incoming' => 0)"); + }); + + test('lock name is scoped to the syslogdb_default global', function () use ($_functions_src): void { + // Lock name includes $syslogdb_default to prevent cross-database + // lock collisions when multiple Cacti instances share a MySQL server. + expect($_functions_src)->toContain('$syslogdb_default . \'.preprocess_incoming\''); + }); + + test('lock name is hashed with sha256', function () use ($_functions_src): void { + // GET_LOCK names are capped at 64 chars; sha256 hex is exactly 64. + expect($_functions_src)->toContain("hash('sha256'"); + }); +}); + +describe('syslog_preprocess_incoming_records — UPDATE statement contract', function () use ($_functions_src): void { + test('flags records with uniqueID via prepared statement', function () use ($_functions_src): void { + // The UPDATE must use a placeholder, not string interpolation. + expect($_functions_src)->toContain('SET `status` = ?'); + expect($_functions_src)->toContain("WHERE `status` = 0"); + }); + + test('UPDATE uses syslog_db_execute_prepared, not syslog_db_execute', function () use ($_functions_src): void { + // Verify the hardened call site uses the prepared variant. + // Extract the preprocess function body for a scoped check. + preg_match( + '/function syslog_preprocess_incoming_records\s*\(\s*\)\s*\{(.+?)^\}/ms', + $_functions_src, + $m + ); + $body = $m[1] ?? ''; + expect($body)->toContain('syslog_db_execute_prepared'); + }); +}); diff --git a/tests/Unit/SyslogReportSqlTest.php b/tests/Unit/SyslogReportSqlTest.php new file mode 100644 index 0000000..4ebb2f8 --- /dev/null +++ b/tests/Unit/SyslogReportSqlTest.php @@ -0,0 +1,59 @@ + 'messageb', 'message' => 'test_message']; + $result = syslog_get_report_sql($report); + + expect($result['sql'])->toContain("WHERE message LIKE ?"); + expect($result['params'])->toContain("test_message%"); + expect($result['sql'])->toContain('FROM `cacti_syslog`.`syslog`'); + }); + + test('syslog_get_report_sql() handles type messagec (contains)', function () { + $report = ['type' => 'messagec', 'message' => 'test_message']; + $result = syslog_get_report_sql($report); + + expect($result['sql'])->toContain("WHERE message LIKE ?"); + expect($result['params'])->toContain("%test_message%"); + }); + + test('syslog_get_report_sql() handles type messagee (ends with)', function () { + $report = ['type' => 'messagee', 'message' => 'test_message']; + $result = syslog_get_report_sql($report); + + expect($result['sql'])->toContain("WHERE message LIKE ?"); + expect($result['params'])->toContain("%test_message"); + }); + + test('syslog_get_report_sql() handles type host', function () { + $report = ['type' => 'host', 'message' => 'test_host']; + $result = syslog_get_report_sql($report); + + expect($result['sql'])->toContain("WHERE sh.host = ?"); + expect($result['params'])->toContain("test_host"); + }); + + test('syslog_get_report_sql() handles type sql (raw sql)', function () { + $report = ['type' => 'sql', 'message' => 'host_id = 5']; + $result = syslog_get_report_sql($report); + + expect($result['sql'])->toContain("WHERE (host_id = 5)"); + }); +}); diff --git a/tests/regression/issue252_xss_output_test.php b/tests/regression/issue252_xss_output_test.php new file mode 100644 index 0000000..7c36ea0 --- /dev/null +++ b/tests/regression/issue252_xss_output_test.php @@ -0,0 +1,45 @@ +')") === false || + strpos($functionsJs, ".text(hostData.host);") === false || + strpos($functionsJs, "$('#host').append(option);") === false) { + fwrite(STDERR, "Expected DOM-safe host option rendering in js/functions.js.\n"); + exit(1); +} + +if (strpos($functionsJs, "$('#host').append('');") !== false) { + fwrite(STDERR, "Legacy unsafe host option HTML concatenation still present.\n"); + exit(1); +} + +print "issue252_xss_output_test passed\n"; diff --git a/tests/regression/issue254_partition_table_locking_test.php b/tests/regression/issue254_partition_table_locking_test.php index 2f02021..1c38a75 100644 --- a/tests/regression/issue254_partition_table_locking_test.php +++ b/tests/regression/issue254_partition_table_locking_test.php @@ -12,7 +12,7 @@ * whose first argument contains 'information_schema' to exclude the * GET_LOCK / RELEASE_LOCK uses of syslog_db_fetch_cell_prepared. */ $partition_query_count = preg_match_all('/syslog_db_fetch_(?:row|assoc|cell)_prepared\s*\([^)]*information_schema/', $functions); -if ($partition_query_count === false || $partition_query_count !== 3) { +if ($partition_query_count === false || $partition_query_count !== 4) { fwrite(STDERR, "Partition queries are not consistently scoped to the requested table.\n"); exit(1); } diff --git a/tests/regression/issue259_csrf_purge_test.php b/tests/regression/issue259_csrf_purge_test.php new file mode 100644 index 0000000..a879294 --- /dev/null +++ b/tests/regression/issue259_csrf_purge_test.php @@ -0,0 +1,106 @@ + breakout in HTML script context +if (strpos($setup, 'JSON_HEX_TAG') === false) { + fwrite(STDERR, "json_encode() must use JSON_HEX_TAG to prevent script-context breakout.\n"); + exit(1); +} + +if (strpos($setup, 'JSON_HEX_AMP') === false) { + fwrite(STDERR, "json_encode() must use JSON_HEX_AMP to escape ampersands in script context.\n"); + exit(1); +} + +if (strpos($setup, 'JSON_HEX_APOS') === false) { + fwrite(STDERR, "json_encode() must use JSON_HEX_APOS.\n"); + exit(1); +} + +if (strpos($setup, 'JSON_HEX_QUOT') === false) { + fwrite(STDERR, "json_encode() must use JSON_HEX_QUOT.\n"); + exit(1); +} + +// Verify user-facing message does not expose CSRF internals (log message may use "CSRF") +if (strpos($setup, "raise_message('syslog_error', __('CSRF") !== false) { + fwrite(STDERR, "User-facing raise_message must not expose CSRF internals to end users.\n"); + exit(1); +} + +// Verify generic user-facing message is present +if (strpos($setup, "Invalid request. Please try again.") === false) { + fwrite(STDERR, "Fail-closed branch must use generic 'Invalid request. Please try again.' message.\n"); + exit(1); +} + +// Verify fail-closed raise_message uses MESSAGE_LEVEL_ERROR severity +if (strpos($setup, "raise_message('syslog_error', __('Invalid request. Please try again.', 'syslog'), MESSAGE_LEVEL_ERROR)") === false) { + fwrite(STDERR, "Fail-closed branch raise_message must use MESSAGE_LEVEL_ERROR severity.\n"); + exit(1); +} + +// Verify log message does not expose internal function name +if (strpos($setup, 'csrf_check() unavailable') !== false) { + fwrite(STDERR, "Log message must not name internal validation function.\n"); + exit(1); +} + +print "issue259_csrf_purge_test passed\n"; diff --git a/tests/regression/issue260_remove_eval_callback_test.php b/tests/regression/issue260_remove_eval_callback_test.php new file mode 100644 index 0000000..3d48607 --- /dev/null +++ b/tests/regression/issue260_remove_eval_callback_test.php @@ -0,0 +1,45 @@ + $root . '/syslog_alerts.php', - 'removal_import' => $root . '/syslog_removal.php', - 'report_import' => $root . '/syslog_reports.php', -); +$target = $root . '/functions.php'; -foreach ($targets as $func => $target) { - $content = file_get_contents($target); +$content = file_get_contents($target); - if ($content === false) { - fwrite(STDERR, "Failed to load $target\n"); - exit(1); - } - - /* - * 1. The request variable must be captured into a local first. - * Whitespace-only input falls through only because trim() is applied - * to the local; if the assignment were missing the condition would - * be wrong. - */ - if (!preg_match('/\$import_text\s*=\s*get_nfilter_request_var\s*\(\s*\'import_text\'\s*\)/', $content)) { - fwrite(STDERR, "$func: \$import_text assignment via get_nfilter_request_var missing in $target\n"); - exit(1); - } +if ($content === false) { + fwrite(STDERR, "Failed to load $target\n"); + exit(1); +} - /* - * 2. The branch condition must trim the local variable, not the raw - * request call. This is what makes whitespace-only values fall - * through to the file-upload branch. - */ - if (!preg_match('/trim\s*\(\s*\$import_text\s*\)\s*!=\s*\'\'/', $content)) { - fwrite(STDERR, "$func: trim(\$import_text) != '' condition missing in $target\n"); - exit(1); - } +/* + * 1. The branch condition must trim the request call. This is what makes + * whitespace-only values fall through to the file-upload branch. + */ +if (!preg_match('/trim\s*\(\s*get_nfilter_request_var\s*\(\s*\'import_text\'\s*\)\s*\)\s*!=\s*\'\'/', $content)) { + fwrite(STDERR, "syslog_get_import_xml_payload: trim(get_nfilter_request_var('import_text')) != '' condition missing in $target\n"); + exit(1); +} - /* - * 3. Inside the textbox branch, $xml_data must be assigned the - * untrimmed local. A non-empty payload is preserved as-is. - */ - if (!preg_match('/\$xml_data\s*=\s*\$import_text\s*;/', $content)) { - fwrite(STDERR, "$func: \$xml_data = \$import_text assignment missing in $target\n"); - exit(1); - } +/* + * 2. Inside the textbox branch, it must return the + * untrimmed local. A non-empty payload is preserved as-is. + */ +if (!preg_match('/return\s+get_nfilter_request_var\s*\(\s*\'import_text\'\s*\)\s*;/', $content)) { + fwrite(STDERR, "syslog_get_import_xml_payload: return get_nfilter_request_var('import_text') assignment missing in $target\n"); + exit(1); +} - /* - * 4. The file-upload branch must still exist (elseif on $_FILES). - * Ensures the fallback path was not accidentally removed. - */ - if (!preg_match('/elseif\s*\(\s*\(\s*\$_FILES\s*\[/', $content)) { - fwrite(STDERR, "$func: \$_FILES elseif branch missing in $target\n"); - exit(1); - } +/* + * 3. The file-upload branch must still exist (if on $_FILES). + * Ensures the fallback path was not accidentally removed. + */ +if (!preg_match('/if\s*\(\s*isset\s*\(\s*\$_FILES\s*\[/', $content)) { + fwrite(STDERR, "syslog_get_import_xml_payload: \$_FILES if branch missing in $target\n"); + exit(1); } -echo "issue269_import_text_branch_logic_test passed\n"; +print "issue269_import_text_branch_logic_test passed\n"; diff --git a/tests/regression/issue269_import_text_trim_check_test.php b/tests/regression/issue269_import_text_trim_check_test.php index 023a930..2485a36 100644 --- a/tests/regression/issue269_import_text_trim_check_test.php +++ b/tests/regression/issue269_import_text_trim_check_test.php @@ -1,45 +1,27 @@