=== WP Defense Scanner ===
Contributors: esteka
Tags: security, scanner, vulnerabilities, hardening, malware
Requires at least: 6.0
Tested up to: 6.7
Requires PHP: 7.4
Stable tag: 0.6.3
License: GPLv2 or later

Defensive security scanner: known-vulnerability lookup, core file integrity verification and configuration hardening audit, with scheduled scans and email alerts.

== Description ==

WP Defense Scanner audits a WordPress installation from the defender's side:

* **Known vulnerabilities** — checks the installed core, every plugin, every theme and the running PHP version against the free WPVulnerability database (no API key needed).
* **Core file integrity** — compares every core file against the official wordpress.org MD5 checksums and flags modified or missing files (a classic indicator of an injected backdoor), plus PHP files dropped inside `wp-content/uploads`.
* **Per-file analysis and quarantine** — any uploads finding can be drilled into: each PHP file gets a verdict (malicious / needs review / no hostile pattern) with the exact signals and line numbers, recognized-benign patterns (compiled Twig or Blade caches, index guards), the external URLs it references checked against known-compromised CDNs, plus size, mtime and SHA-256. You then quarantine file by file — reversible rename, never deletion. A live canary test reports whether the server actually executes PHP in uploads, and the automatic fix verifies its own .htaccess block by re-testing.
* **Hardening audit** — dashboard file editor enabled, debug output shown to visitors, default `admin` user, more than two administrators, auto-updates disabled, no fallback theme, default table prefix, XML-RPC exposure, missing HTTPS, default auth salts, world-readable `wp-config.php`, world-writable directories, open registration with elevated default role, inactive plugins, end-of-life PHP, 3-2-1 backup reminder.
* **Proportional advice** — the report matches recommendations to the site's actual risk profile. An upstream WAF/CDN (Cloudflare or a self-hosted Coraza/BunkerWeb + CrowdSec stack) is suggested only when something justifies it (a store, open registration, many accounts); a simple blog is told plainly that it probably does not need one, and that no self-hosted option stops a large volumetric DDoS.
* **External exposure probes** — the plugin requests its own site like an attacker's scanner would: missing security headers (nosniff, frame-ancestors, Referrer-Policy, HSTS), directory listing on uploads/plugins/wp-content, public user enumeration via the REST API and `?author=N` redirects, and whether `xmlrpc.php` actually answers from outside.
* **Scheduled scans** — a daily WP-Cron scan; new critical/high findings are emailed to the site administrator.
* **Report UI** — a Tools → Defense Scanner report with severities and recommended fixes, plus a dashboard summary widget.
* **Languages** — English and Spanish (es_ES). Translation sources live in `languages/`; see `bin/make-pot.sh`.

Extensible: register additional checks through the `wpds_checks` filter.

Website: https://estekadigital.com/wp-defense-scanner — Source: https://gitlab.airean.net/esteka/wp-defense-scanner

== Roadmap ==

* One-click mitigations (disable XML-RPC, block REST user enumeration and `?author=N` redirects, block PHP execution in uploads, hide post authors).
* Login protection: rate limiting / lockouts, mandatory 2FA for administrators, custom login path, HTTP Basic (htpasswd) helper for the backend.
* Activity/audit log of admin actions.
* Malware signature scan of plugin/theme files (obfuscation heuristics: eval/base64/gzinflate chains).
* WP-CLI command and JSON export (for log-reading/cleanup agents on self-hosted servers).

== Changelog ==

= 0.6.3 =
* Fixed the missing "Enable auto-updates" toggle: WordPress only offers it for plugins whose update provider describes them on every check, and ours answered "nothing" whenever no newer version existed. The current version is now always reported, so core lists the plugin under no_update, shows the toggle, and can auto-update it.
* Fixed new releases taking up to six hours to appear even after "Check again": the feed was cached for six hours and the flush hook never fired on that button. The cache now lasts one hour and is bypassed entirely when WordPress runs a forced check.

= 0.6.2 =
* Updater hardening now that the source is public: the feed's version must be a plain dotted number and the package URL must be HTTPS, on the update host, and end in .zip — anything else is ignored rather than handed to WordPress.
* The deployment target (host, port, path) no longer lives in the repository: `bin/publish.sh` reads it from a git-ignored `bin/publish.env` (see `bin/publish.env.example`). A public repository should not document how its own update host is administered.
* Stray Python bytecode removed from version control and ignored.
* `LICENSE` file added with the GPL-2.0-or-later text the plugin header has always declared; the repository is public and a licence that is only mentioned is not a licence.

= 0.6.1 =
* Critical fix: the vulnerability lookups never worked. The plugin queried `api.wpvulnerability.com/v1`, which answers 404 to every route; the live host is `www.wpvulnerability.net`. Before 0.4.4 the failure was cached as "no vulnerabilities"; since 0.4.4 it surfaced as "unverified". Every scan now really checks core, plugins, themes and PHP against the database.
* Plugins and themes closed in the WordPress.org directory are reported as High, with the reason the directory gives: closures are frequently due to security problems, and a closed component receives no more updates.
* PHP end of life now comes from the support window the API publishes for each branch, with the exact date, plus a Low warning when the date is less than 90 days away. The hardcoded threshold only remains as a fallback when the API is unreachable.
* Releases now ship a stable `wp-defense-scanner-latest.zip` next to the versioned file, so download links never go stale, and `bin/publish.sh` uploads all three artifacts and verifies from the outside that the feed parses and both ZIPs answer.

= 0.6.0 =
* Autoloaded options: a new scan step measures what WordPress loads on every request and attributes each option to its probable owner — core, an installed plugin or theme, a transient, or nothing installed, which is how leftovers from long-removed plugins show up. "Analyze options" lists the heaviest ones and lets you stop autoloading them one by one, reversibly. Core options are locked, because WordPress reads them on every request and taking them out of autoload only adds queries. Nothing is ever deleted from that screen.
* Expired transients that pile up without a persistent object cache get a one-click, risk-free cleanup: they are past their expiry and regenerate on demand.
* The generic Site Health "autoloaded options" summary is no longer imported; this check supersedes it with detail and actions.

= 0.5.0 =
* Spanish (es_ES) translation of the whole interface, report and emails — all 353 strings. Translations ship inside the plugin and load from `languages/` on `init`, as core requires since 6.7; since the plugin is self-hosted, translate.wordpress.org never sees it, so this is the only way they can arrive.
* Both `.mo` and the PHP translation format WordPress prefers since 6.5 (`.l10n.php`, parsed by OPcache instead of decoded on every request) are shipped; the `.po`/`.pot` sources stay in the repository, out of the ZIP.
* Reproducible i18n tooling in `bin/`: `make-pot.sh` regenerates the template from the sources with GNU gettext and merges existing translations; `compile-mo.sh` validates every `.po` with `msgfmt --check` (which also catches mismatched sprintf placeholders) and builds both binary formats.
* Fixed the update feed: `build.sh` assembled `info.json` in a shell heredoc, so a changelog with quotes or several bullets produced invalid JSON that the updater rejected silently — no site would ever have been offered an update. The feed is now written with `json.dumps` and parsed back before publishing.

= 0.4.4 =
* Fixed a potentially site-breaking bug in the uploads .htaccess fix: `php_flag` and `Require all denied` were written bare, which on Apache with PHP-FPM (no mod_php) or on Apache 2.2 makes the whole uploads directory answer 500 — every image gone — while the verification still passed, because a 500 also "does not execute PHP". Directives are now wrapped in `<IfModule>` blocks, a plain-text probe proves static files are still served, and when they are not the change is rolled back automatically and reported as such.
* "Could not check" is no longer reported as "clean". A vulnerability API outage used to be cached for an hour as an empty list, and exposure probes that failed (loopback blocked, TLS, DNS) returned nothing. Every probe that cannot run now produces an explicit `unverified:` finding naming exactly what was not checked. An API failure stops further lookups for that run instead of waiting 15 seconds per component, and a 404 (component unknown to the database, normal for custom or premium code) is treated as an answer, not as an outage.
* The PHP canary used to test execution in uploads is now removed by a shutdown hook even if the request dies mid-way, leftovers from earlier runs are swept at the start of each scan, and the uploads scan ignores the plugin's own probe names.

= 0.4.3 =
* The score ring matches core's compact shape: a small ring inline with its verdict, instead of an oversized one with the label underneath.

= 0.4.2 =
* Fixed the score ring rendering nearly empty at a high score: the fill was set as an HTML attribute, which a stylesheet always overrides, and core's site-health.css sets the same property. It is now an inline style, both on page load and while scanning.
* The ring and its verdict are laid out as a centred column instead of drifting to opposite edges of the header.

= 0.4.1 =
* Scan progress moved into the header, the way Site Health does it: the status text appears where the verdict will be, naming the check being run, and the separate progress bar under the button is gone.
* The ring is determinate rather than a spinner — it fills with real progress as steps complete, so a long scan does not look stalled.
* The score ring is painted by this plugin's own stylesheet instead of relying on core's: green when the site is in good shape, amber when it should be improved, red when something critical is open, and blue while scanning. It was rendering as an empty outline before.

= 0.4.0 =
* Automatic fixes can now edit wp-config.php, safely: the new contents are built in memory and parsed with the PHP tokenizer to prove they are valid before anything is written, the original is backed up into the protected quarantine directory (never as a .bak next to wp-config.php, which the web server would hand out as plain text), and the swap is a single atomic rename that preserves permissions. If the result would not parse, the edit is discarded and the file is left untouched.
* "Fix automatically" for the file editor now writes DISALLOW_FILE_EDIT into wp-config.php, and only falls back to the runtime block when the file is read-only — which the report states plainly rather than claiming a permanent fix.
* The "blocked by this plugin, not by wp-config.php" finding gained a fix that promotes the runtime block to the constant.
* Debug output shown to visitors gained a fix that sets WP_DEBUG_DISPLAY to false.
* A wp-config.php that is not writable by the web server is reported as the good thing it is, with the exact line to paste by hand.
* Before editing, the constant is looked for in every shape it can take. One plain define on its own line is rewritten — and the report names the previous value, so a deliberate setting is never changed silently. A conditional define, a const, or several occurrences are refused with the line number: rewriting those is unsafe, and appending would only add a duplicate. A constant defined outside wp-config.php (an included local config, a PHP auto_prepend file) is refused too, because PHP keeps the first definition and the fix would look applied while changing nothing.

= 0.3.5 =
* Fixed a false positive: "background updates are not working as expected" was reported as High while Site Health itself said the site was fine. That test is one of core's "async" tests — it makes an authenticated request to the site using the logged-in user's cookies, so running it headlessly from cron or an AJAX scan fails for the wrong reason. Async tests are no longer executed; a single INFO finding names them and links to Site Health, where the browser runs them properly.
* Site Health tests are now enumerated from core's own registry instead of a hardcoded list, so every direct test is imported (including ones this plugin never listed) and the split stays correct across WordPress versions.
* Imported findings carry core's badge, so a performance note reads as "Site Health · Rendimiento: …" instead of being mistaken for a security issue.
* A third-party Site Health test that throws no longer aborts the scan step.

= 0.3.4 =
* The default wp_ table prefix finding is now INFO, not LOW, and says plainly that changing it does not prevent SQL injection — an injection able to read data can read information_schema.tables. It advises leaving it alone on a live site and only changing it during a migration that already has a maintenance window.
* Per-finding "Re-check" button: re-runs only the check that produced that finding and merges the result into the stored report, so verifying a fix no longer means a full scan. Resolved findings are struck through, with a link to refresh the report.
* Findings now record which scan step produced them, which is what makes targeted re-checks possible.

= 0.3.3 =
* Fixed "Fix automatically" for the file editor appearing to do nothing: the mitigation worked, but the check kept testing the DISALLOW_FILE_EDIT constant instead of the effective capability, so the finding never cleared. The check now asks wp_is_file_mod_allowed(), which reflects the constant, this plugin's mitigation and any other plugin's filter alike.
* Mitigations are hooked the moment they are enabled, not on the next request, so a fix can verify its own result immediately — and fix_disable_file_editor now reports failure if another plugin forces file modifications back on.
* When the editor is blocked by the plugin rather than by wp-config.php, the report says so and offers the constant as the permanent version, instead of staying silent.
* Fix results never render an empty box: missing messages fall back to a clear outcome plus a reminder to re-scan.

= 0.3.2 =
* Security fix in the plugin itself: the quarantine directory was named predictably and relied on an .htaccess that nginx and IIS ignore, so quarantined source code could be downloaded by anyone who guessed the URL. The directory now carries a random token in its name (existing quarantines are migrated automatically), and a new scan step requests a quarantined file from outside to report whether it is actually reachable — with a server-specific deny block when it is.
* The nginx snippet for blocking PHP in uploads now uses a "^~" prefix location with a nested rule. A plain regex location silently loses to the PHP handler declared earlier in the vhost, which made the block appear applied while PHP kept executing.

= 0.3.1 =
* Web server detection centralized and made accurate: it now distinguishes the server that executes PHP (which decides whether .htaccess is honored) from the edge server seen from outside, so an nginx proxy in front of Apache is not mistaken for plain nginx.
* The "Fix automatically" button for PHP execution in uploads is no longer offered on servers that ignore .htaccess (nginx, IIS). Those get a ready-to-paste server block with a Copy button instead, generated for the site's real uploads path.
* Findings can carry a configuration snippet, rendered as a copyable code block.

= 0.3.0 =
* Deep file analysis: "Analyze files" gives a per-file verdict (malicious / needs review / no hostile pattern) with the exact signals and line numbers, recognized-benign patterns (compiled Twig/Blade caches, index guards), referenced external URLs flagged against known-compromised CDNs (polyfill.io and friends), size, mtime and SHA-256 — then quarantine per file, one decision at a time.
* Live test of PHP execution in uploads: writes a harmless canary, requests it, removes it. A webshell there only matters if the server runs it, so the report says which it is — and the automatic fix writes an .htaccess block and re-tests to prove it worked (and says so honestly when nginx ignores it).
* Findings can carry a deep link: Site Health findings open the exact test, update/plugin/user findings open the right admin screen.
* Uploads findings split three ways by verdict instead of one blanket CRITICAL.

= 0.2.0 =
* Site Health-style report UI: score circle, tabs, accordion findings; welcome/About page shown after activation.
* Progressive AJAX scan with a progress bar narrating each step.
* One-click fixes: reversible runtime mitigations (disable XML-RPC, file editor, user enumeration, security headers), wp-config chmod, official core-file restore with quarantine backup, and webshell quarantine.
* Core Site Health tests imported into the report (PHP version, SQL server, page cache, cron, background updates, loopback).
* Fewer false positives: locale-modified version.php recognized; uploads PHP classified by webshell-trait heuristics (guards and plugin caches no longer critical).

= 0.1.0 =
* Initial release: vulnerability lookup, core integrity, hardening audit, daily cron, email alerts, report page.
