Rekuest CVE Client
A WordPress plugin that checks the site against the Rekuest CVE scanner and reports the result to Zabbix.
Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/rekuest/wp-rekuest-cve-client/archive/refs/heads/main.zipRekuest CVE Client
A WordPress plugin that checks the site against the Rekuest CVE scanner and reports the result to Zabbix.
The plugin builds an inventory of the site and sends it to the scanner, which returns the known vulnerabilities affecting the installed versions. The plugin then publishes the outcome on a token-protected JSON endpoint. The same format ("schema 1") is used by the Laravel package and by the script for Plesk servers, so a single Zabbix template covers every client.
Note: the plugin is only useful if you have access to an instance of the Rekuest CVE scanner (URL and token are provided by Rekuest). Without a scanner it does nothing. The code is public for transparency and so that sites can update from GitHub releases. The admin interface is in Italian.
Contents
- Features
- Requirements
- Installation
- Configuration
- Status endpoint for Zabbix
- WP-CLI
- Plugin updates
- Security
- FAQ
- Development
- Release
- License
Features
-
Complete inventory:
- WordPress and PHP versions;
- every installed plugin and theme, including inactive ones, because their files stay reachable over HTTP;
- Plesk version (from the version file, or set manually);
- Composer packages (
vendor/composer/installed.json, Composer 1 and 2, Bedrock included).
-
Scanner check at most once every 55 minutes, with caching, a lock against concurrent requests and a retry after an error.
-
An unreachable scanner never makes the site look clean: the endpoint reports the error and keeps the last valid result.
-
Status endpoint for Zabbix on two transports:
- REST
rekuest-cve/v1/status; admin-ajax.phpas an alternative.
It is protected by a token, an IP allowlist and a limit on failed attempts.
- REST
-
"Settings → Rekuest CVE" page with:
- the last result and the vulnerable packages;
- the Verifica ora ("Check now") button;
- status token generation;
- an inventory preview.
-
"Vulnerabilità note" ("Known vulnerabilities") dashboard widget.
-
WP-CLI commands for checks, inventory and diagnostics.
-
Automatic updates from GitHub releases, with the package's ed25519 signature verified before installation.
-
Multisite: network activation, a single inventory and a single endpoint on the main site.
-
No runtime dependencies. The plugin sends no email and changes nothing on the site: Zabbix handles notifications.
Requirements
| Minimum | Notes | |
|---|---|---|
| WordPress | 5.0 | Automatic updates need WordPress 5.2+ or PHP 7.2+, to verify the signature with libsodium. |
| PHP | 5.6 | Tested up to PHP 8.4. |
| Scanner | Rekuest CVE with the /api/v1/ API |
URL and token are provided by Rekuest. |
Installation
-
Download
rekuest-cve-client.zipfrom the latest release. Do not use the "Source code" archive generated by GitHub: it is the source tree, not the installable package. -
In WordPress go to Plugins → Add New → Upload Plugin and activate the plugin. On multisite, network-activate it.
-
Configure the scanner in one of two ways:
- with constants in
wp-config.php(recommended); - from the Settings → Rekuest CVE page (on multisite: Network Settings → Rekuest CVE).
define( 'REKUEST_CVE_SCANNER_URL', 'https://cve-scanner.example.com' ); define( 'REKUEST_CVE_SCANNER_TOKEN', '…' ); // provided by Rekuest define( 'REKUEST_CVE_STATUS_TOKEN', '…' ); // generated for this site: wp rekuest-cve token - with constants in
-
If the status token is not in
wp-config.php, generate it with the Genera token di stato ("Generate status token") button. The token is shown only once: copy it right away. -
Press Verifica ora ("Check now"), or run
wp rekuest-cve check --force. -
In Zabbix, create a host with:
- Host name equal to the host shown on the settings page;
- template "Rekuest CVE by HTTP";
- macro
{$CVE.STATUS.URL}= the endpoint URL; - macro
{$CVE.STATUS.TOKEN}= the status token.
Configuration
Every setting can be defined as a constant in wp-config.php or on the settings page. A constant takes precedence, and the matching field on the page becomes read-only.
| Constant | Default | Description |
|---|---|---|
REKUEST_CVE_SCANNER_URL |
— | Base URL of the scanner, without /api/…. https:// only: with an http:// URL the client counts as not configured. |
REKUEST_CVE_SCANNER_TOKEN |
— | Scanner bearer token. |
REKUEST_CVE_STATUS_TOKEN |
— | Status endpoint token, at least 32 characters. Empty or too short: endpoint disabled. |
REKUEST_CVE_HOST |
home_url() domain |
Value of the host field, equal to the Zabbix "Host name". On multisite the default is the main site's domain. |
REKUEST_CVE_ALLOWED_IPS |
no restriction | IPs or CIDRs (IPv4/IPv6) allowed on the endpoint, comma-separated. |
REKUEST_CVE_CACHE_TTL |
3300 |
Result freshness, in seconds (minimum 60). |
REKUEST_CVE_TIMEOUT |
15 |
Timeout of the scanner call, in seconds (1–120). |
REKUEST_CVE_VERIFY_SSL |
true |
Scanner certificate verification. Can be turned off only with this constant, and only for local development. |
REKUEST_CVE_PLESK_VERSION |
detected | Plesk version, when /usr/local/psa/version is not readable (the usual case with open_basedir). |
REKUEST_CVE_INCLUDE_COMPOSER |
true |
Send the Composer packages found. |
REKUEST_CVE_UPDATES |
true |
false disables updates from GitHub releases (e.g. sites managed with Composer). |
REKUEST_CVE_AUTO_UPDATE |
WordPress decides | true/false forces or prevents automatic updates of the plugin. |
Filters:
| Filter | Default | Purpose |
|---|---|---|
rekuest_cve_composer_vendor_dirs |
4 paths | vendor/ directories where installed.json is looked up. |
rekuest_cve_plesk_version_file |
/usr/local/psa/version |
Path of the Plesk version file. |
rekuest_cve_retry_after |
300 |
Seconds to wait after a scanner error. |
rekuest_cve_status_rate_limit |
30 |
Failed-token attempts allowed per IP per minute. |
Status endpoint for Zabbix
| Transport | URL |
|---|---|
| REST (primary) | GET /wp-json/rekuest-cve/v1/status. With "Plain" permalinks: /?rest_route=/rekuest-cve/v1/status. |
| Alternative | GET /wp-admin/admin-ajax.php?action=rekuest_cve_status |
- Authentication:
X-Cve-Status-Token: <token>header.Authorizationis not used because on Plesk with PHP under FastCGI/FPM it often does not reach PHP. - Forced refresh:
?refresh=1, ignored if the last attempt was less than 60 seconds ago.
Responses:
| Code | When |
|---|---|
200 |
Authorized access. Always 200: a failed check is reported in the body (check_ok: 0, error). |
401 |
Missing or wrong token. |
403 |
IP not allowed (with REKUEST_CVE_ALLOWED_IPS). |
404 |
Status token not configured. The route does not exist: the site responds as if the plugin were not installed. |
429 |
More than 30 failed-token attempts in one minute from the same IP (or the same IPv6 /64 network). Sent with Retry-After. |
Responses carry Cache-Control: no-store, private and X-Robots-Tag: noindex, and page-cache plugins are bypassed (DONOTCACHEPAGE).
Body ("schema 1", abridged):
{
"schema": 1,
"client": { "type": "wordpress", "name": "rekuest-cve-client", "version": "1.0.0" },
"host": "www.example.com",
"check_ok": 1,
"error": null,
"last_attempt_at": "2026-11-07T16:04:46Z",
"last_success_at": "2026-11-07T16:04:46Z",
"age_seconds": 120,
"from_cache": true,
"inventory": { "platform": 2, "composer": 0, "wordpress_plugins": 18, "wordpress_themes": 2 },
"result": { "status": "vulnerable", "severity_level": 3, "max_cvss": 8.8, "packages": [ … ] }
}
result is the scanner response, forwarded as is: fields the scanner adds in the future reach Zabbix without a plugin update.
Security plugins and WAFs
-
Plugins that block the REST API for anonymous users: the plugin automatically exempts its own route from the
rest_authentication_errorsfilter and from the "Disable REST API" plugin. If the REST API is still blocked (Wordfence, Solid Security or WAF rules), you have two options:- add
rekuest-cve/v1to the security plugin's exceptions; - point Zabbix at the
admin-ajax.phpURL.
wp rekuest-cve statusshows whether the endpoint responds as expected. - add
-
ModSecurity on Plesk: if Zabbix gets a 403 with an HTML page (rather than the plugin's
{"message": …}JSON), the WAF blocked the request.- Confirm with
curl -i -H 'X-Cve-Status-Token: <token>' https://www.example.com/wp-json/rekuest-cve/v1/status: an HTML 403 comes from the WAF, a JSON 403 from the plugin. - In Plesk, under Tools & Settings → Web Application Firewall (or in the domain's log), find the ID of the rule that blocked the request.
- Disable that rule for the domain, or point Zabbix at
admin-ajax.php.
- Confirm with
WP-CLI
| Command | Description |
|---|---|
wp rekuest-cve check [--force] [--format=table\|json] |
Refreshes and shows the summary and the vulnerable packages. Exit code 0 on success, 1 on failure. |
wp rekuest-cve inventory [--format=table\|json] |
Shows the inventory without calling the scanner. |
wp rekuest-cve status |
Diagnostics: configuration (masked tokens), endpoints, Plesk file, REST API, result age, next cron event, updates. |
wp rekuest-cve token |
Generates a status token and prints it, without saving it. |
Plugin updates
Updates come from this repository's releases, with no third-party library and no token on the sites.
- Manifest: every 12 hours the plugin reads
update.jsonfrom the latest release (releases/latest/download/update.json). The manifest holds the version, the ZIP URL, its SHA-384 hash and the signature. - Notification: if the version is newer and the requirements are met, the update shows up in Dashboard → Updates like any other plugin. It follows WordPress's automatic update setting (or
REKUEST_CVE_AUTO_UPDATE). - Verification before installation: the plugin downloads the ZIP and verifies its hash and ed25519 signature. The signature covers slug, version and hash, so a package that has been tampered with, swapped, or passed off as another version is rejected. The public key ships with the plugin (
Rekuest_Cve_Updater::PUBLIC_KEY); the private key exists only as a secret of the release workflow. - No same-name plugin from wordpress.org: the plugin is never looked up on wordpress.org (
Update URIheader, and the plugin is removed from the update request), so a same-slug plugin published there cannot replace it.
For testing, another manifest can be used with the REKUEST_CVE_UPDATE_MANIFEST_URL constant. The signature is still required, so a different manifest cannot get unsigned code installed.
With WordPress < 5.2 and PHP < 7.2, libsodium is missing and the signature cannot be verified. In that case the plugin offers no updates and must be updated manually by uploading the new ZIP (Plesk WP Toolkit can do this in bulk).
Security
The plugin is designed not to add attack surface to sites that are frequent targets of automated scans.
- Endpoint invisible when not configured: without a status token, the REST route and the AJAX action are not registered. Even when enabled, the endpoint is not listed in the
/wp-json/index, andOPTIONSrequests and the namespace index respond as for a non-existent route. - Tokens:
- compared in constant time (
hash_equals); - the status token must be at least 32 characters (the generated one has 64, from
random_bytes); - never shown on the page after generation, never included in responses or error messages.
- compared in constant time (
- No writes for anonymous requests: only attempts with a wrong token are counted, in a single size-bounded option. The database does not grow even under scans from thousands of IPs.
- No CSRF bypass: the exemption for security plugins applies to the exact route only, and the request continues as anonymous.
- Scanner connection:
- https only, with certificate verification;
- redirects are not followed, because they would carry the token elsewhere;
- response capped at 1 MB.
- Scanner data treated as external input:
- non-scalar values are discarded, and all output is escaped;
- links are allowed only towards the scanner itself.
- Admin area: nonce and capability checks on every action (
manage_network_optionson multisite), safe redirects, PHP files not directly executable. - Release package: it contains no
readme.txt, changelog or development files, so it does not reveal its version to fingerprinting scanners. Updates are signed (see above).
To report a vulnerability, read SECURITY.md. Do not open a public issue.
FAQ
How often is the scanner queried?
At most once every 55 minutes (the cache TTL): Zabbix polls the site every hour and always finds a result due for renewal. An hourly WP-Cron event refreshes the result even without Zabbix. After an error, the next attempt waits 5 minutes, or the scanner's Retry-After if longer.
What if WP-Cron is disabled (DISABLE_WP_CRON)?
The result is still refreshed on the first Zabbix poll after the TTL expires.
Can an unreachable scanner make the site look clean?
No. check_ok is 0, error holds the reason and result keeps the last valid result.
What is left after uninstalling? Nothing: options (network options included), the lock, the attempt counter, transients and the cron event are all removed.
Development
rekuest-cve-client.php header, constants, autoloader, bootstrap
uninstall.php complete data removal
includes/
class-plugin.php services, hooks, activation/deactivation, WP-Cron
class-options.php network options on multisite, autoload = no otherwise
class-settings.php wp-config.php constants / rekuest_cve_settings option
class-inventory.php core, plugins, themes, PHP, Plesk, Composer
class-scanner-client.php scanner call and error mapping
class-state.php persistent state and atomic lock (INSERT IGNORE)
class-status-service.php refresh algorithm and "schema 1" JSON
class-status-endpoint.php REST + admin-ajax, token, IPs, attempt limit
class-updater.php signed updates from GitHub releases
class-admin-page.php settings page
class-dashboard-widget.php "Vulnerabilità note" widget
class-view.php presentation helpers
class-cli.php WP-CLI commands
tests/ PHPUnit with the WordPress test suite
bin/ test setup, build, versioning, signing
Classes use the Rekuest_Cve_ prefix and a custom autoloader. The syntax is PHP 5.6-compatible (no scalar type hints, ??, void, arrow functions…), checked by PHPCompatibilityWP.
Design decisions
- State in an option, not a transient: with an external object cache, transients can disappear, while the last valid result must survive.
- Result stored as raw JSON and forwarded decoded as an object, so empty objects
{}and new fields reach Zabbix unchanged. - Lock with
INSERT IGNORE:add_option()usesINSERT … ON DUPLICATE KEY UPDATEand is not atomic under concurrent requests. The lock outlasts the call timeout. - REST authorization computed once per request: WordPress also calls the
permission_callbackfor theAllowheader. - Bedrock: besides
ABSPATH/vendor,dirname(ABSPATH)/vendorandWP_CONTENT_DIR/../vendor, the plugin also looks indirname(dirname(ABSPATH))/vendor.
Commands
You need PHP, Composer and MySQL/MariaDB for the test suite.
composer update # dev dependencies (no composer.lock: CI uses several PHP versions)
vendor/bin/phpcs # WordPress-Extra + PHPCompatibilityWP (PHP 5.6+)
bin/install-wp-tests.sh wp_tests root password 127.0.0.1 latest
vendor/bin/phpunit
WP_MULTISITE=1 vendor/bin/phpunit
install-wp-tests.shacceptslatest,trunk,X.Y.ZorX.Y;X.Yresolves to the latest patch of that branch, which includes the test suite fixes.- WordPress 5.0 supports PHPUnit up to 6.
composer.jsonignores advisoryPKSA-z3gr-8qht-p93vfor old PHPUnit versions, which PHP 5.6/7.x need. PHPUnit never ends up in the ZIP.
At Rekuest, commands run in Rocker containers, e.g. rk run composer update or rk run php:8.4 vendor/bin/phpcs. The test database is rocker-mariadb-11, user root/rocker.
CI matrix:
| PHP | WordPress | Mode |
|---|---|---|
| 5.6 | 5.0 | single site |
| 7.4 | 5.9 | single site |
| 8.1 | 6.4 | single site |
| 8.3 | latest | single site |
| 8.4 | latest | single site |
| 8.4 | latest | multisite |
Release
Every push to main produces a release, with no manual steps.
-
Full CI (PHPCS and the test matrix). If it fails, nothing is released.
-
Version calculation with
bin/next-version.sh, from the latestvX.Y.Ztag and the commit messages in Conventional Commits format:Commit Version fix: …,chore: …,docs: …, …patch ( 1.4.2→1.4.3)feat: …minor ( 1.4.2→1.5.0)feat!: …orBREAKING CHANGE:in the bodymajor ( 1.4.2→2.0.0)For a deliberate version jump, raise
Version:in the plugin header: if it is higher than the calculated one, the header wins. -
ZIP: the version is written into the header and the constant in the working copy (no commit to
main), then the ZIP is built without the files listed in.distignore. -
Signature: the ZIP is signed with ed25519 and
update.jsonis written. The script refuses to sign if the key does not match the public key in the plugin. -
Publication: the
vX.Y.Ztag and the GitHub release are created, withrekuest-cve-client.zip,update.jsonand notes generated from the commits.
With [skip release] in the commit message, that push does not produce a release.
Repository setup (one-time)
releaseenvironment: under Settings → Environments, create thereleaseenvironment, restricted to themainbranch, with theREKUEST_CVE_SIGNING_KEYsecret (the base64 private key).- Branch protection on
main: require pull requests and the CI checks. - Immutable releases: under Settings → General → Releases, so that published assets cannot be replaced.
- Private vulnerability reporting: under Settings → Security, for SECURITY.md.
Rotating the signing key
- Generate a new key pair with
php bin/signing-key.php /secure/path/key. The script prints the public key. - Release a version that contains the new public key in
Rekuest_Cve_Updater::PUBLIC_KEY, still signed with the old key. - Once the sites have updated, replace the
REKUEST_CVE_SIGNING_KEYsecret.
If the private key is compromised, sites must be updated manually with a ZIP that contains the new key.
License
GPL-2.0-or-later, like WordPress. © Rekuest.