=== Bulwarden ===
Contributors: bulwarden
Tags: maintenance, security, backup, malware, monitoring
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.40.2
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

Site agent for health monitoring, malware scanning, encrypted backups and remote updates, driven by a Bulwarden hub you connect it to.

== Description ==

Bulwarden is the **site agent**, installed on each managed WordPress site. It
exposes a signed REST API (`jgm/v1`) that a **Bulwarden hub** calls to read
health and security data, run checks and trigger updates. The hub is a separate
application — it is not part of this plugin — and **this plugin talks to no hub
at all until you pair it with one**. On its own it sends nothing anywhere.

It is built for the person who is responsible for a site they did not build:
an agency looking after forty of them, or an owner who wants one place that
answers "is this thing all right?" without logging into it.

= What it reports =

* **Site health** — WordPress, PHP and plugin versions, pending updates, disk
  usage, and WordPress' own Site Health tests, collected in one snapshot.
* **A full plugin and theme inventory**, with each item's version, whether it
  is active, whether an update is waiting, and whether it came from
  WordPress.org at all — which is what lets a hub match it against a CVE feed.
* **Must-use plugins**, which WordPress runs on every single request with
  nothing activating them and no way to switch one off from the admin. Nothing
  else in wp-admin lists them properly, and dropping a file there is the tidiest
  way to install a backdoor.
* **Configuration-level security checks** with a posture score — file editing,
  directory listing, XML-RPC, debug output, and the rest of the settings that
  quietly decide how exposed a site is.
* **Unfinished updates.** An update that already replaced a site's files can
  still fail to finish, and neither state clears itself: a pending database
  schema upgrade that every administrator is sent to and the public site hides
  completely, or a leftover `.maintenance` lock from an updater that died
  mid-copy. Both are reported, and both can be finished from the hub by running
  exactly what WordPress itself would have run.
* **Email delivery.** WordPress keeps no record of the mail a site sends: when
  a contact form stops delivering or an order confirmation never arrives, the
  error is raised and thrown away. This counts what was attempted, what failed
  and why, and reports which transport is doing it — PHP's `mail()`, an SMTP
  server, or the plugin that configured one. No part of any message is stored:
  not the recipient, not the subject, not the body.

= What it finds =

* **A malware scan in three layers.** Every core and WordPress.org file is
  verified against its *published* checksums, so a modified file is found
  whether or not anybody has written a signature for what was put in it; then
  every file is read for the code patterns webshells and injected backdoors
  actually use. Findings carry the file, the line and a short excerpt.
* **Backdoors that arrive dressed as an ordinary plugin** — a forged
  administrator login, a privileged action gated on a key baked into the file,
  an account created from hard-coded credentials, users hidden from the users
  list. A plugin absent from WordPress.org has no published files to check it
  against, so without this a hostile one surfaced only as *unverifiable*.
* **A plugin hiding itself.** Malware that conceals a plugin does it by
  answering the question before WordPress can — and the hidden plugin is then
  also the one plugin whose files are never checked. The scan counts what is on
  disk itself and compares.
* **Server-configuration files.** `.htaccess`, `php.ini` and `.user.ini` are
  the one part of a WordPress install that changes what the *server* does
  rather than what WordPress does, and nothing in wp-admin shows them. A file
  set to run before every request, a rule that makes uploaded images execute as
  PHP, a redirect that only fires for search engines: all reported, with the
  line. Nothing is ever changed for you — a good share of those directives are
  written deliberately by a host or a security plugin, and the point is that
  you get to decide which.
* **Leftover files.** An interrupted backup, a finished restore and a migration
  plugin all leave things behind and none of them clean up: a
  `wp-config.php.bak` the web server hands out as plain text, a `database.sql`
  in the web root, an installer that overwrites the site with no login. Found,
  measured, and removable from the hub — and the *site* decides what may be
  deleted, so a stale list can only ever be refused.
* **Signatures that keep up.** The scan's pattern list can be updated by your
  provider without updating this plugin, so a new detection reaches the site in
  hours rather than waiting for a release to travel a whole fleet. A site that
  has never been paired scans with the list this plugin ships.
* **A scan that starts itself.** When a plugin WordPress.org does not publish
  appears in the plugins or must-use folder, the site starts a scan within
  seconds and tells your provider what set it off, instead of waiting for the
  next scheduled one. Fake plugins are how most WordPress break-ins arrive, and
  a scan is a cheap thing to be wrong about: nothing is blocked, quarantined or
  deleted — the site looks, and your provider decides.

= What it does =

* **Remote core, plugin and theme updates**, wrapped in maintenance mode so
  visitors never see a half-updated site — and taken off the host's PHP time
  limit, so a core update on shared hosting finishes instead of being killed
  part-way.
* **Zero-knowledge encrypted backups.** The archive is built *and encrypted on
  this server*, under a passphrase that is never transmitted anywhere;
  only the ciphertext is uploaded. Nobody — including whoever runs the hub —
  can read a backup without that passphrase.
* **Restores that fail safely.** The same thing in reverse, and it can put back
  the database alone if that is all that broke. Every table is rebuilt beside
  the live one and swapped in only once it is complete, so a restore stopped by
  a timeout or a server limit leaves the database exactly as it was rather than
  half-replaced — and picks itself back up where it stopped. What could not be
  done is counted and reported rather than discarded, so a restore that ran into
  trouble says so instead of reporting success.
* **Page tracing.** Your provider can ask this site to render one of its own
  public pages and report which file produced each line of the HTML — core, a
  plugin, the theme, a must-use plugin. It answers the question nothing in
  WordPress answers: *which* plugin adds the Google Fonts tag, or the tracking
  script, or the markup nobody can find a setting for. The page is never
  altered to do it.
* **WP-Cron on a schedule that actually runs.** A site with no visitors runs no
  cron, which is how scheduled posts, order emails and licence checks quietly
  stop.
* **A support card on the dashboard** naming whoever maintains the site, how to
  reach them, and a *Request help* button that files a ticket with them
  directly — attached to the right site, without the owner having to explain
  which one.
* **An activity log on the site itself**, append-only, recording everything that
  changed this site or sent part of it somewhere — and which side asked. Updates
  installed by your provider and anything that failed with them, backups,
  restores, scans, repairs, deleted leftovers, a file read off the server, a
  traced page, and every change to the credentials, the passphrase or the
  provider details. WordPress records what changed and never who asked, and the
  answer belongs on your own server rather than only in somebody else's console.

= Pairing =

On activation the plugin provisions an API key + secret. Copy the **connect
string** from the **Bulwarden** entry in the WordPress admin menu and paste
it into the hub's "Connect a site" form. Your provider's support details are
then pushed to the site automatically by the hub and appear on the dashboard.

No hub account yet? Until the site is paired, the plugin's banner and settings
screen offer **Start a free trial**, which opens the Bulwarden hub's pricing
page (https://hub.bulwarden.net/pricing/) in a new tab. Start a trial there,
set your password from the email you receive, choose "Connect a site" in the
hub and paste the connect string. Opening that page sends nothing from this
site; it is an ordinary link.

= Security =

Hub → client requests are authenticated with the per-site API key and an
HMAC-SHA256 signature over the method, route, timestamp, a single-use nonce and
the body. The nonce is what makes a signature good exactly once: it is recorded
when the request arrives and refused if it comes back, so a captured request
cannot be replayed. Timestamps outside a 5-minute window are rejected outright,
which is also how long a nonce has to be remembered for. The secret never
travels on the wire — it lives only in this plugin and in the hub webapp.

== Privacy ==

Bulwarden is a client agent: on its own it sends data nowhere. Pairing this
site with a hub (yours, or your maintenance provider's) is an explicit,
opt-in action — you choose when to copy the connect string from the Bulwarden
screen in the WordPress admin menu and hand it to that hub. Nothing is
transmitted before that.

Once paired, data only moves in these cases:

* **Health, inventory, and security checks.** The paired hub reads these from
  this site's signed REST API when it polls or when a check is requested —
  WordPress/plugin/theme versions and update status, a full plugin & theme
  inventory, disk usage, and the configuration-level security posture score.
* **Backups**, only when triggered (the Settings screen's "Back up now"
  button, or a request from the paired hub): the full site is archived and
  encrypted on this server with a passphrase that is never transmitted
  anywhere, then the ciphertext is uploaded to the hub. Only someone who has
  the passphrase can decrypt a backup — the hub operator cannot read its
  contents.
* **Malware scan results**, only when a scan is triggered (admin- or
  hub-initiated): findings (type, severity, file path, line, and a short
  excerpt) are sent to the hub that requested the scan.
* **Finishing an interrupted update**, when the paired hub asks. Nothing is
  sent in that exchange; the site runs WordPress' own database upgrade, or
  deletes an expired `.maintenance` lock file, and reports the outcome.
* **Server-configuration files**, when the paired hub asks: the contents of
  this site's `.htaccess`, `php.ini` and `.user.ini` files, so the hub can show
  what is in them and when they last changed. These are configuration, not
  content — no post, page, media file or user data is read.
* **Email counters**, as part of the health snapshot: how many messages this
  site attempted and how many failed over the last seven days, the error text
  the mail server returned, and which transport is sending. **No part of any
  message is recorded** — not the recipient, not the subject, not the body.
* **A traced page**, only when the hub asks for one: the HTML of a single
  public page of this site, exactly as an anonymous visitor would receive it,
  plus the file paths and line numbers that produced it. No file contents are
  read and no logged-in view is ever rendered.

Independently of any hub pairing, this plugin verifies WordPress core and
wordpress.org-hosted plugin files by comparing local file hashes against the
public checksum APIs at `api.wordpress.org` and `downloads.wordpress.org`.
Those requests carry only what identifies the release to look up — the
WordPress version and locale, and each plugin's slug and version. No file
contents, hashes, URLs or user data are sent: the reference checksums come
back and the comparison happens here, on this server.

Bulwarden does not call home on activation, does not collect analytics, and
stores data on no server this site hasn't been explicitly connected to. The
hub itself is a separate service outside this plugin's control; consult your
provider for how it handles the data described above.

== External services ==

This plugin connects to the services below. Nothing here runs on activation.

= Your Bulwarden hub =

Bulwarden is the site-side agent for a **hub** — the management application an
agency or site owner runs to watch and maintain a fleet of WordPress sites. The
hub is a separate product and is not part of this plugin.

The default hosted hub is operated by Bulwarden at https://bulwarden.net/ —
terms of use: https://bulwarden.net/terms/ , privacy policy:
https://bulwarden.net/privacy/ . A hub can also be self-hosted, or run by the
maintenance provider you buy from, in which case this plugin talks to that
provider's server instead and their terms and privacy policy apply.

**No connection to any hub exists until you make one.** You pair a site by
copying the connect string from the Bulwarden screen in the WordPress admin
menu and pasting it into the hub; before that the plugin contacts no hub at
all, and it never discovers or chooses one on its own. Once paired, data moves only in these cases:

* **When the hub asks for status.** It reads this site's signed REST API and
  receives WordPress/plugin/theme versions and update status, the full plugin
  and theme inventory, disk usage, the results of WordPress' own Site Health
  tests, and the configuration-level security posture score.
* **When a backup runs** (you press "Back up now", or the hub requests one).
  The site is archived and encrypted here, with a passphrase that is never
  transmitted, and only the ciphertext is uploaded. The hub operator cannot
  read a backup's contents without that passphrase.
* **When a malware scan runs** (started by you or by the hub). The findings —
  type, severity, file path, line number and a short excerpt — are sent to the
  hub that asked for the scan.
* **When the hub pushes your provider's support details** — their name, logo,
  support address and accent colour — on connect and whenever they change
  them. Nothing is sent from the site in that exchange.

Every one of those requests is authenticated with this site's own API key and
an HMAC-SHA256 signature; the shared secret never travels over the wire.

= api.wordpress.org and downloads.wordpress.org =

The malware scan verifies WordPress core and wordpress.org-hosted plugins
against their published checksums, using WordPress.org's own public APIs at
`https://api.wordpress.org/core/checksums/1.0/` and
`https://downloads.wordpress.org/plugin-checksums/`. This happens whenever a
scan runs, whether or not the site is paired with a hub.

These requests identify only the release being looked up: the WordPress version
and locale, and each installed plugin's slug and version. No file contents,
hashes, site URL or user data are sent — the reference checksums are returned
and compared locally.

These are WordPress.org services, covered by the WordPress.org privacy policy:
https://wordpress.org/about/privacy/

== Installation ==

Bulwarden is one half of a pair: this plugin runs on the site, and a **hub**
runs somewhere else and manages it. Installing the plugin on its own does
nothing until you pair it with a hub.

1. Install and activate the plugin as you would any other &mdash; upload it under
   Plugins &rarr; Add New &rarr; Upload Plugin, or install it from the directory.
2. Go to **Bulwarden** in the WordPress admin menu. The plugin generated an
   API key and secret for this site on activation; the screen shows a single
   **connect string** that carries the site name, URL and both credentials.
3. Copy that string and paste it into your hub's "Connect a site" form. That is
   the whole pairing step &mdash; there is nothing to type on this side, and no
   hub address to configure here.
4. If you want encrypted backups, set a **backup passphrase** on the same
   screen. It never leaves this server, and it is required to restore. Keep it
   somewhere you will still have it after the disaster you are backing up
   against &mdash; nobody, including your hub operator, can recover a backup
   without it.

= Do I need a hub? =

Yes, for anything beyond installing it. The hub is separate software: use the
hosted one, run your own, or use the one your maintenance provider runs. Until
the site is paired, this plugin contacts nothing and does nothing.

= Upgrading =

Updates install like any other plugin. Your credentials, passphrase and settings
survive an update; nothing needs re-pairing.

== Frequently Asked Questions ==

= Does this plugin send my site's data anywhere on its own? =

No. Before you pair it with a hub it contacts nothing at all, and it never
discovers or chooses a hub by itself. After pairing, data moves only for the
things listed under Privacy below &mdash; health checks, backups, malware scan
findings &mdash; and only to the hub you paired with.

= Can the hub operator read my backups? =

No, provided you set a passphrase. The archive is encrypted **on this server**
with a key derived from your passphrase, and only the ciphertext is uploaded.
The passphrase is never transmitted, so a hub holds bytes it cannot decrypt.
The same property is why nobody can recover a lost passphrase for you.

= What happens if I deactivate or delete the plugin? =

The site stops answering its hub, which the hub will notice and report as the
agent going unreachable. Deleting the plugin removes its settings, its audit-log
table and its working directories (see `uninstall.php`); backups already stored
on a hub are unaffected.

= Is the plugin available in German? =

Yes. Everything the plugin puts on screen &mdash; the settings page, the support
card on the dashboard, every status message and every scan finding &mdash; is
translated, and WordPress picks it up from your site's own language setting with
nothing to configure. Both German locales are covered: *Deutsch* and *Deutsch
(Sie)*. The wording is formal throughout, because these screens are read by the
owner of the site rather than by the agency that maintains it.

= Does it work on multisite? =

The plugin runs per site. A network-wide database upgrade is reported but not
performed &mdash; core's own network upgrade screen walks every site in the
network, and that is where it belongs.

Network activation is not supported: it sets the plugin up for the main site
only, and every other site in the network would have no credentials to be
paired with. Activate it on each site you want managed, from that site's own
Plugins screen, and pair each one with the hub separately.

= Why does the malware scan flag a file I know is fine? =

The file-integrity check compares every core and WordPress.org-hosted file
against the checksums WordPress.org published for that exact release. A caching
or optimisation plugin that rewrites a stylesheet produces the same mismatch a
backdoor does, which is why "this file no longer matches what was published" is
reported as a fact rather than a verdict, and why non-executable files are
warnings rather than critical findings. Your hub can show you the actual diff.

= What does it do to my site's performance? =

Nothing on page loads: there is no front-end code path. Backups and malware
scans are real work and run in the background across WordPress cron ticks, to a
time budget, so they do not block visitors. Health checks are answered on demand
when the hub asks.

= Which PHP and WordPress versions are supported? =

PHP 7.4 or newer and WordPress 6.0 or newer. Backups additionally need the
`sodium` extension (encryption) and either `zlib` or the `zip` extension
(archiving); the Settings screen tells you if either is missing.

== Screenshots ==

1. The Bulwarden screen at a glance: whether the site is connected, whether
   backups are encrypted, when the last backup and scan ran, and how current
   the scanner's signature list is.
2. Connecting the site: the connect string you paste into your hub, and the
   API credentials behind it.
3. Backups: a run in progress showing the phase it has reached, its log and a
   cancel button, above the encryption passphrase and the folders left out of
   the archive.
4. The malware scan: what the last one found, grouped by kind, and how much of
   the site could be checked against the files WordPress.org published.
5. The activity log: an append-only record of credential changes, backups,
   restores, scans and repairs, including the ones the hub triggered.
6. The dashboard support card &mdash; who maintains this site, how to reach
   them, and the *Request help* button that files a ticket with them directly.

== Upgrade Notice ==

= 1.23.0 =
The malware scan gains eighteen checks written against real backdoors, and eight
existing checks stop reporting honest code. If your provider's hub had refused an
update as infected, it will go through after this.

= 1.22.2 =
Nothing on your site changes. This release exists to correct what the plugin's
own listing says about it, which had fallen behind several releases.

= 1.22.1 =
Fixes backups of sites larger than 4 GB, which failed while verifying the
finished archive. If you have such a site, run a backup after updating.

= 1.22.0 =
A restore that cannot finish no longer leaves your site half-restored, and a
restore that ran into trouble now says so instead of reporting success. Nothing
you had set up changes; no action needed.

= 1.21.0 =
The activity log now records the updates your provider installs on this site,
and three other things it was missing. Nothing else changes; no action needed.

= 1.20.0 =
A plugin that appears on your site without WordPress.org publishing it now
starts a malware scan by itself, within seconds, instead of waiting for the next
scheduled one. Nothing is blocked or deleted; no action needed.

= 1.19.0 =
Adds German, server-configuration checks, email delivery monitoring and a
cancel button for a stuck scan, and stops a slow update on shared hosting being
killed part-way. Nothing you had set up changes; no action needed.

= 1.18.0 =
The malware scan now recognises backdoors that arrive dressed as an ordinary
plugin, not just obfuscated webshells. Nothing you had set up changes; no action
needed.

= 1.17.1 =
Fixes false "a WordPress core file is missing" and "no longer matches the
official release" findings on the readme and licence files a non-English
WordPress installs under a translated name.

= 1.16.0 =
The support card on your dashboard can now show your provider's telephone
number, if they have set one in their hub. Nothing you had set up changes; no
action needed.

= 1.15.0 =
The malware scan now counts the plugins on disk itself instead of taking
WordPress' word for what is installed, so a plugin that has hidden itself from
the plugin list is reported &mdash; and checked against WordPress.org like every
other one. Must-use plugins are now listed too: they run on every request and
the admin gives you no way to switch one off, so the scan reports what is there.
Nothing you had set up changes; no action needed.

= 1.14.0 =
Adds a "Request help" button to the dashboard support card, and lets your hub
find and clear the files old backups, restores and migrations left behind.
Nothing you had set up changes; no action needed.

= 1.13.7 =
Identical to 1.13.5 and 1.13.6 in behaviour. Nothing on your site changes.

= 1.13.6 =
Identical to 1.13.5 in behaviour. It exists so the plugin directory's listing
and its screenshots describe the same version; nothing on your site changes.

= 1.13.5 =
Corrections to the Bulwarden screen rebuilt in 1.13.4. Nothing changes in how
the plugin works; no action needed.

= 1.13.4 =
The Bulwarden screen has been rebuilt around what you actually came to it for.
Nothing you had set up changes; no action needed.

= 1.13.3 =
Bulwarden moves out of the Settings submenu into its own entry in the WordPress
admin menu. Nothing else changes; no action needed.

= 1.13.2 =
Fixes a false "a WordPress core file no longer matches the official release"
finding on `wp-includes/version.php` &mdash; raised on any site whose display
language differs from the WordPress package it was installed from.

= 1.13.1 =
Fixes a false "a plugin file no longer matches the version published on
WordPress.org" finding on plugins WordPress.org publishes more than one copy of.

= 1.13.0 =
Your hub can now see and finish an update that changed this site's files but
never completed &mdash; a pending database upgrade or a leftover maintenance
lock. Restores can also put back only the database, leaving files untouched.

= 1.11.0 =
Must be installed alongside the matching hub update: the two sign requests to
each other and the signing form changed on both sides at once.

= 1.9.0 =
Stored data moved to a new prefix and there is no automatic migration &mdash;
re-pair the site with your hub after updating, and re-enter the backup
passphrase. Existing backups still need the old passphrase to decrypt.

== Changelog ==

= 1.40.2 =
* Maintenance: internal code-style fixes only, no change to how the plugin behaves.

= 1.40.1 =
* Improved: the plugin lock now also covers must-use plugins. When a site is locked, a must-use plugin dropped in after the lock is moved out of the mu-plugins folder instead of being allowed to run.

= 1.40.0 =
* New: your maintenance provider can set a site to "lock" plugins — no new plugins can be installed or activated at all. Existing plugins keep working and still receive updates, and you can still deactivate a plugin; only adding or switching on a new one is blocked. This is off unless your provider turns it on.

= 1.39.0 =
* New: if a plugin that is not from the WordPress.org directory appears on your site and then crashes every page, Bulwarden now switches it off by itself and brings the site back, instead of leaving it down until someone notices. It only does this to a plugin that arrived in the last few days; the files are left in place so they can be examined.
* New: your maintenance provider can set your sites to hold a new plugin that is not from the WordPress.org directory — switching it off the moment it appears and turning it back on only once a scan is clean, or only once the provider approves it. This is off unless your provider turns it on. A plugin you install yourself is held for a moment, not blocked.
* Improved: the malware scan now recognises a plugin that creates a hidden account and gives it administrator powers while calling it an "editor".

= 1.38.1 =
* Improved: the malware scan now recognises a fake plugin that creates a hidden account with full administrator rights while calling it an "editor", so it does not look like a new administrator. This is what the fake "maintenance toolkit" plugins installed on several sites in October 2026 do. Such a plugin is now reported as malware.

= 1.38.0 =
* New: the malware scan now finds PHP code hidden in ".cache" files, and in any file that an .htaccess tells the server to run as PHP whatever its name. This is how the malware in the compromised Rank Math SEO PRO 3.0.123 update stays on a site, and until now the scan never opened those files. Such a file is reported as a warning, or as critical when the server is set up to run it or the code looks like an attack. Cache files that only store data are not reported.
* New: the server-configuration check now reads .htaccess rules that span several lines, and says which kinds of file a rule makes the server run as PHP. A rule that makes data, cache or image files run as PHP is reported as critical.
* New: the malware scan reports a folder in your site's main directory that has the same name as one of your published pages (for example "about-us") and contains an index file. The web server shows that folder instead of your real page, which is how some spam kits take over a page without touching WordPress. It is a warning, or critical when the folder shows search engines something different from visitors.

= 1.37.5 =
* Improved: if you installed Bulwarden yourself from the plugin directory, the "not connected yet" banner, the setup steps on the Bulwarden screen and the dashboard card now offer a "Start a free trial" button. It opens the Bulwarden hub in a new tab, where you can start a trial and then connect this site with its connect string. Nothing is sent from your site when you click it. If a provider asked you to install the plugin, connect the site to them as before; the button disappears once the site is connected.

= 1.37.4 =
* Fixed: on sites behind a page cache with few visitors, a malware scan could take a day or more to finish, because each step of the scan waited for somebody to visit the site before the next one started. The scan now starts its next step itself, so it finishes in minutes. Restores already worked this way and now share the same mechanism.
* Improved: malware scans are much faster on sites with large media libraries. Images, videos, PDFs and other media files in the uploads folder are still checked for hidden PHP code, but the full set of malware signatures is only run on them when they actually contain PHP. On a test site with 9,000 images a scan went from five minutes to under two.

= 1.37.3 =
* Fixed: on sites where another plugin or a code snippet restricts the WordPress REST API to logged-in users (for example the "Disable REST API" option in Admin and Site Enhancements), every request from the maintenance hub was refused before Bulwarden could check it, so the site could not be paired, monitored or backed up. Requests signed by the hub are now let through that restriction, for Bulwarden's own routes only; the rest of the REST API stays restricted, and nobody is logged in.

= 1.37.2 =
* Fixed: the malware scan was meant to look for PHP files in the cache folder (wp-content/cache) since 1.35.0, but on every site using the default backup settings it never did, because that folder is left out of backups by default. Leaving the cache out of backups no longer keeps the scan out of it. Only PHP files there are checked, so the scan does not slow down on a large page cache. If you want part of the cache left alone, exclude that subfolder instead.

= 1.37.1 =
* Fixed: on sites with large media folders the malware scan could keep re-reading the same folder and never finish. A folder that needs more than one pass now continues where the previous pass stopped, so every file is read once.
* Fixed: if the web host stopped the scan's background process abruptly, the scan was left half-finished with nothing to resume it. The scan now arranges its next step before it starts working, so it picks up again by itself within ten minutes.

= 1.37.0 =
* New: your maintenance provider can ask the plugin what it is doing right now — whether a backup, malware scan or restore is running, how far it has got, and whether the scheduled tasks it waits for are running on time. Nothing is started or changed by asking, and no passwords, keys or file contents are included in the answer.

= 1.36.1 =
* Fixed: signing in with one click from your maintenance provider could end on the message "This sign-in link is not valid any more" even though the sign-in had worked — on sites where you were already signed in, and on sites using a custom login address. The link is still used exactly once; a browser that already holds an administrator session is now taken straight to the dashboard instead of being shown the error.

= 1.36.0 =
* New: your maintenance provider can check more than the homepage after an automatic update. The plugin lists the site's pages — shop pages such as the checkout and the cart are marked — so the provider can pick the ones that matter, and each is compared before and after the update.
* New: for pages that only show their real content to a signed-in visitor, such as a customer account, the provider can ask the site to create a read-only account called bulwarden-check. It can do nothing but read, cannot sign in with a password, and is only ever signed in for a few minutes by the automatic check. If its role is ever given more rights, the site stops signing in as it. Deleting the account switches this off.
* Every creation of that account and every check sign-in is recorded in the activity log.

= 1.35.0 =
* New: the malware scan now checks the `wp-content/cache` folder for PHP files with code in them. Attackers who exploit a caching plugin write their web shells there, and the scan used to skip the folder entirely. Only PHP files are read, so a large page cache costs little; generated pages, stylesheets and images are never opened. Every PHP file found there is also checked against the full signature table.
* Compiled Blade views (Sage/Acorn), compiled Twig templates (Timber) and generated data files are recognised and not reported.
* A very large cache is reported as not fully checked, rather than treated as clean.

= 1.34.1 =
* Maintenance release: code-style fixes only, with no change in behaviour.
  It carries everything listed under 1.34.0.

= 1.34.0 =
* Security: **a long backup can no longer undo settings changed while it
  ran.** Regenerating the connection credentials, setting a backup
  passphrase or a change of hub address during a backup used to be reverted
  when the backup finished.
* Security: signed requests from your agency's hub now refuse anything added
  to the address that the signature does not cover, and each request stays
  single-use for its whole validity window.
* Security: **the site keeps more secrets out of what it sends your agency.**
  Scan excerpts from configuration copies, database dumps, exports and logs
  are redacted; password files and connection strings in server
  configuration are no longer sent; files that look like keys, dumps or
  backups are never handed out for a comparison; and a failed restore's
  error messages no longer quote the data that failed.
* Fix: **restores now finish correctly on Windows hosting** and other servers
  that store table names in lower case. Tables with capital letters in
  their name (Wordfence's, for example) kept their old contents before.
* Fix: the site shows its maintenance notice while a restore writes files
  back, not only while it writes the database.
* Fix: a malware scan that takes longer than an hour is no longer restarted
  from the beginning every hour, and a new scan no longer runs alongside the
  one it replaced.
* Fix: the leftover-file check no longer offers a running restore's working
  folder for deletion.
* Hardening: a restore's working files now have unpredictable names, and the
  copy of this site's connection details it keeps is removed as soon as it
  has been used.
* Fix: a restore that runs out of disk space now stops and says so, instead
  of reporting half-written files as restored.
* Fix: a restore that fails part-way no longer leaves a second copy of the
  database behind in temporary tables, and backups never include them or a
  restore's working folder.
* Fix: a malware scan that runs out of memory on the site, or cannot save its
  progress, is now reported as failed instead of never finishing.
* Fix: restores protect this plugin's own folder and working folders on sites
  with a customised folder layout, and a relative `auto_prepend_file` that
  works is no longer switched off after a restore.
* Security: the site only sends backups and scan results to, and fetches
  restores from, the hub it is paired with; a restore rejects a backup file
  with impossible sizes before reading it; signed requests and one-click
  sign-in links can no longer be used twice by two requests arriving at the
  same moment; the health report no longer lists subscribers and shop
  customers; and deleting the plugin now also removes its working folders.
* Multisite: network activation is not supported — activate the plugin on
  each site you want managed.

= 1.33.1 =
* Security: **malware scan excerpts sent to your agency no longer include
  passwords in a few more cases** the earlier protection missed: settings
  stored as plain text, passwords written in capital letters, passwords
  containing an escaped quote, and logins inside web addresses. Posts
  behind a password, and form entries some plugins store as posts, are now
  treated as private.
* Fix: an interrupted plugin update can no longer make a folder-specific
  scan rule run on every file.

= 1.33.0 =
* New: **the malware scan now reports program code added to a plugin's or the
  active theme's folder.** For every plugin published on WordPress.org, and for
  the active theme, the scan already checks that each file the release contains
  is unchanged; it now also reports a PHP file in that folder that the release
  does not contain at all, which is where an attacker hides a backdoor next to
  genuine files. Empty guard files and protected data files are not reported.

= 1.32.0 =
* New: **the malware scan now reports PHP files with code in the uploads
  folder.** Attacks through vulnerable file-upload features in plugins almost
  always leave a small PHP file there, and it stays behind after the plugin
  is updated. Harmless files that plugins put there on purpose, such as empty
  "Silence is golden" files, protected data files and known plugin files like
  Borlabs Cookie's, are not reported.

= 1.31.1 =
* Security: a password in `wp-config.php` or in a site setting is now always
  left out of scan findings, however short it is. Before, a password shorter
  than six characters could appear in a finding.
* Fix: if an update of this plugin was interrupted halfway, leaving old and
  new files side by side, a malware scan could stop with an error. It now
  finishes with the plugin's own detection rules until the update completes.

= 1.31.0 =
* New: **the malware scan now also checks the database.** Many attacks never
  leave a file behind: they put a script into a post, hide code in a site
  setting, create an extra administrator or schedule a task that brings the
  attack back. The scan now looks at posts, settings, user accounts and
  scheduled tasks, and reports an administrator account that exists but is
  hidden from the Users screen.
* New: **your provider can now respond to a new attack within hours instead
  of waiting for a plugin update.** Besides the code patterns it already
  sends, it can now send the file names an attack drops, extra types of
  program file to report, patterns for the database checks above, and rules
  that apply only to certain folders or file types.
* New: a scan now always uses your provider's latest detection rules. Before,
  a site could run for up to six hours on the previous set.
* Security: findings from `wp-config.php` and from the database never include
  passwords, keys or other secrets. The finding still shows what matched,
  with the secret replaced by its length.

= 1.30.1 =
* Fix: one malware signature, the one that looks for `preg_replace()` with the
  long-removed `/e` modifier, could take far longer than it should on files
  containing long regular expressions, and on some files gave up without an
  answer. It now checks those files quickly and reliably, and still finds
  what it is meant to find.

= 1.30.0 =
* New: **the malware scan now recognises files that known attacks leave
  behind by their name.** Some exploits drop a marker or a web shell under a
  fixed name, and the name is often the only thing about the file that never
  changes. The first entries cover the WordPress file-inclusion attack
  (CVE-2026-87902) seen in September 2026.
* New: **the scan now also looks in the server's temporary folders** (`/tmp`,
  `/var/tmp`, `/dev/shm`), where that attack parks its payload before using
  it. Only files your own site created are looked at, so on shared hosting
  other customers' files are never read or reported. A PHP file there that
  matches no known signature is reported as information only, because some
  template engines legitimately keep compiled files there.

= 1.29.3 =
* Fix: **a restore that could not write some files still reported a clean
  finish.** A folder the web server had lost write access to meant dozens or
  hundreds of files quietly not restored, under a green "Finished" badge. The
  files it deliberately leaves alone — your `wp-config.php`, the maintenance
  plugin itself, WordPress' drop-ins — still count as a clean restore, as they
  should; the ones it tried and failed to write now say so.
* Fix: **"remove files the backup does not contain" now says which folder it
  could not clean out.** It deliberately never empties a folder your backup has
  nothing for — otherwise a backup taken without your plugins folder would
  delete every plugin on the site — but it only mentioned that in the run log.
  Since this is the option you would use to clear out a hack, and must-use
  plugins are where one usually hides, the restore now names the folder it
  skipped where you will see it.
* Fix: **a file that is a shortcut (symlink) is now listed when a backup skips
  it.** Backups never follow shortcuts, which is correct, and skipped folders
  were already listed; skipped *files* were not — so a site whose uploads or
  configuration files are linked in from elsewhere had them missing from the
  backup with nothing saying so.
* Fix: a backup that failed to download during a restore reported the raw
  network error. It now says the backup could not be fetched and that nothing
  on the site was changed, with the technical detail after it.

= 1.29.2 =
* Fix: **a backup corrupted every percent sign in your site's content and
  settings, and a restore put the corruption back.** Each `%` was written into
  the backup as a 66-character marker, so prices like "50% off", CSS widths and
  percent-encoded links came back mangled — and any stored setting that
  contained one (widget configuration, theme options, many plugins' settings)
  became unreadable and was silently lost, on a restore that reported success.
  Backups taken from this release on are correct; ones taken before it still
  carry the fault.
* Fix: **a restore could stop part-way through the database and leave the site
  half-restored.** A table whose full name was 52 to 55 characters long could
  not be swapped into place, which ended the restore with some tables replaced
  and the rest not. It mostly affected sites with a custom table prefix and
  WooCommerce installed. Such a table is now restored directly and named in the
  report, as three other cases already were.
* Fix: asking a site to restore while it was already busy with a backup or
  restore raised a PHP fatal error instead of politely declining, so the site
  logged a critical error and your maintenance provider was told the site had
  broken rather than that it was busy.

= 1.29.1 =
* Maintenance: coding-standard clean-up in the one-click sign-in code and
  the settings defaults. No behaviour changes.

= 1.29.0 =
* New: **one-click sign-in from your maintenance provider.** Your provider can
  open this site's admin as an administrator without a password, over the same
  signed connection it already uses to update and back up the site. Each
  sign-in uses a link that works exactly once, for one minute, for one named
  administrator, and is written to the activity log with the account it used.
  A switch on the Bulwarden screen, beside the credentials, turns it off — and
  turning it off closes the route, not only the button.

= 1.28.2 =
* Fix: **the progress of a file restore is now reported correctly when it
  finishes.** The step that writes a backup's files back ended by reporting
  that it had written none of them &mdash; a completed restore showed
  "0 of 93,680 files, 0%" beside a count of the files it had just written.
  The two figures were also counted differently: the total included the
  backup's database dump and its description alongside the files, so it could
  never be reached however many files were restored. It now counts the
  backup's files, and every one of them is reported either as written or as
  deliberately left alone &mdash; this site's own wp-config.php, Bulwarden
  itself, and WordPress' drop-ins are never overwritten, and your hub now says
  so instead of leaving the difference unexplained.

= 1.28.1 =
* Fix: **a restore no longer spends most of its time waiting to be asked to
  carry on.** A restore runs in rounds, and at the end of each one the plugin
  asked WordPress' scheduler to come straight back for the next — except that
  the request was made from inside the scheduler, which refuses it. So the work
  only continued when a visitor happened to arrive, or after a three-minute
  safety timer, and on a quiet site — which is most sites being restored — a
  restore took hours of waiting for minutes of work. On sites where WordPress'
  own scheduler is switched off in favour of a real cron job, which is the
  usual advice for a site that matters, nothing continued the restore at all
  between those cron runs. Each round now asks for the next one directly.

= 1.28.0 =
* New: **a banner on a site that has the plugin but is not connected to
  anyone.** Until a site is paired with a maintenance hub the plugin does
  nothing at all — no backups, no scans, nothing watched — and from the
  dashboard that is indistinguishable from a plugin quietly working. Anyone who
  can install plugins now sees a notice saying so, with the way to connect the
  site one click away, and it can be put off for a month at a time. The support
  widget on the dashboard no longer says Bulwarden maintains a site that nobody
  has agreed to maintain.
* New: **a way to ask Bulwarden for help when there is no maintenance provider
  at all.** The Request help button on the dashboard writes to whoever looks
  after the site, which is no use to somebody who installed this and has nobody.
  The plugin's own settings screen now carries a short form that reaches
  Bulwarden directly, and says exactly what it sends: the site's address, its
  WordPress and PHP versions and the reply address typed into it. Nothing else,
  and no keys.
* New: **the hub is told whether a backup encryption passphrase has been set** —
  the fact that there is one, never the passphrase itself, which stays on this
  site and is never sent anywhere. Without one every backup stops before it
  starts, and an agency previously found that out from a failed run at three in
  the morning rather than from the setup nobody finished.
* New: **five more malware detections**, from a study of 400,000 production
  webservers (Kasturi et al., *Mistrust Plugins You Must*): code that redefines
  a built-in PHP function instead of loading the real one, a function name
  assembled from fragments and then called, a file written to disk, made
  executable and run, code that deletes the file it ran from, and the branding
  "nulled" plugin marketplaces stamp into the paid plugins they give away with
  something added.

= Earlier versions =
The changelog for 1.27.3 and earlier is in changelog.txt, shipped with the plugin.
