=== ATV Web Security ===
Contributors: atvsoftwarelabs
Tags: security, protection, malware, two-factor, lockdown
Requires at least: 6.0
Tested up to: 7.1.3
Requires PHP: 8.0
Stable tag: 1.0.49
License: GPL-2.0-or-later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Donate link: https://shop.atvsoftwarelabs.com/product/support-us/

Remove a compromised website's ability to make persistent changes while keeping it fully readable to visitors.

== Description ==

ATV Web Security puts your WordPress website into a "protected" state where visitors can still read and use the site, but nothing can persist changes to it — not plugins, not compromised administrators, not direct database access, and not a PHP shell.

How it works — multiple independent layers:

* WordPress protection — plugin/theme/core operations, file editors, user creation, option writes and administrator actions are intercepted and denied.
* Database protection (manual) — switch it on from the Dashboard layer switches when you want it. The database account's write privileges are REVOKED at the MySQL/MariaDB server level using a separate privileged MySQL account (allowed flows are re-granted only row-level INSERT/UPDATE/DELETE), so even direct SQL from arbitrary code is refused. The site account's own GRANT authority is never consumed, because revoking it would permanently destroy the restore path. An application-layer SQL firewall blocks write queries even when privileges cannot be changed. Selecting a protection tier never turns this on.
* Filesystem protection (manual) — switch it on to make wp-config.php and .htaccess read-only, and to mark those files plus the plugin's signing key immutable with chattr where the host allows it. Original permissions are snapshotted and restored when the lock is switched off. wp-admin, wp-includes, plugins and themes stay writable on purpose so WordPress can still update itself; changes there are detected by integrity monitoring and the new-file watch instead. Selecting a protection tier never turns this on.
* Upload protection — executable uploads are blocked by extension, MIME and content; the uploads directory is hardened so scripts cannot execute there.
* API protection — REST, AJAX, XML-RPC, WP-CLI and cron-based mutation is blocked. Reads remain available.
* Integration with the PHP firewall — traffic under wp-includes is rejected by a small mu-plugin, links to private files are blocked, and low-level PHP tricks are neutralised.
* Integrity monitoring — baseline hashes of core, configuration and plugin files are verified periodically.
* New-file watch — a scheduled scan of the WordPress installation detects files that were not there before, stores them as signed alerts, and reports the ones nothing explains in a dashboard notification and by email. Each finding shows the full file path, the detection time and size, with Dismiss and Delete buttons after review. Files that a WordPress core, plugin, theme or translation update explains — including installs and updates performed outside WordPress — are kept on the Integrity tab with their cause instead of being announced. Database tables are watched the same way. Scan frequency, email delivery and recipients are configurable on the Integrity tab.
* Access rules (optional) — AbuseIPDB-powered website access block by IP reputation score and a login country rule that only allows the admin login page from a country you select. Both are off by default, respect an IP/CIDR allowlist, fail open on lookup errors and cache results to stay within API rate limits.
* Backend lock — the plugin lives in its own top-level admin menu; once protection is ON and a master password is set, the settings page stays hidden behind a lock screen until that password is entered (12-hour unlock, forged-cookie safe, brute-force rate limited). The master password accepts 8-64 letters, digits and symbols, and is the single password accepted to turn protection OFF (older 12-digit passwords keep working).
* Email verification code (Two-Factor, optional) — administrators can be asked for a six-digit code sent to their email address in addition to their WordPress password. The code field appears on the login form itself, right after the password is accepted, so the whole flow stays on one screen with no page refresh. Codes are valid for 10 minutes, can be resent once a minute (five codes per quarter hour), and the account locks for 15 minutes after five wrong entries. If the mailer cannot deliver, the login stays blocked on purpose — recovery is a single command from the shell: wp atvws otp disable.
* Passkeys / WebAuthn (optional, recommended for "only my device") — registers a hardware-backed credential with Windows Hello / TPM, Touch ID, a phone or a security key. The private key is created and used inside the device and never reaches the website, a cookie, JavaScript or a backup. With enforcement on, protected administrator logins are accepted only from a device whose passkey verifies; an account with no passkey is never blocked, and recovery stays available. An account covered by a registered passkey skips the emailed code — either factor alone satisfies the login.
* Trusted device lock (optional) — the portable fallback for browsers without WebAuthn. Binds protected actions to one computer with a one-time token in a secure HttpOnly, SameSite cookie. Any other computer gets a "link your device" screen that requires the master password, and linking/unlinking always requires it. The Recovery tab stays reachable so a lost device can never lock the owner out permanently. A verified passkey also satisfies this gate.
* Behavior policy (optional) — studies privileged activity and locks the session instantly when the risk score crosses a threshold: failed unlock (15), failed disable attempt (25), failed restore attempt (20), failed login (20), refused passkey login (10). Scores are windowed per source IP for 24 hours; the owner (a linked-device or verified passkey session, or an allowlisted IP) is never scored and can clear a lock with the master password.
* Cryptomining defence — four independent layers: every rendered page is checked before it leaves PHP and script tags naming known mining services or pools are removed; WordPress's own HTTP API is refused for pool domains; a time-sliced filesystem scan walks wp-content and reports native executables and miner configuration markers; and a database scan searches posts and options for injected script blocks. The scans are report-only and surface on the Miners tab — nothing is quarantined by a heuristic match.
* Detection & monitoring — a signed baseline of every script and stylesheet the login page loads, with a report the moment a new address appears (login page watch); a scheduled self-probe that asks the site for risky paths (backups, dumps, dotfiles, debug logs) and flags anything the web server would hand to anyone (sensitive file probe); honeypot decoy paths and hidden form fields that trigger escalating, temporary bans for automated scanners; and a local vulnerability inventory that flags plugins and themes that are outdated, abandoned (no release for 2+ years) or no longer listed in the wordpress.org API — computed locally, without phoning home.
* Snapshot backup and restore — a signed copy of every file WordPress can change (core, plugins, themes, uploads, the whole web root and wp-content drop-ins, wp-config.php) plus a complete database dump with the real CREATE TABLE schema of every table. A snapshot is only saved after it validates: every row dump must decode, row counts must match, and the options table must contain a working siteurl — a backup that cannot rebuild a bootable site is never accepted. The "current" snapshot is captured automatically the first time protection is enabled and is what the documented manual recovery procedure uses; keep as many named snapshots as you like (for example before an update) plus one automatic snapshot every day, each independently verifiable, restorable and deletable from the Snapshot tab or WP-CLI. There is no retention limit and nothing is ever deleted automatically. Restore is verified end to end: it requires protection and both manual write locks to be off, refuses a damaged or incomplete snapshot before touching anything, moves files that are not in the snapshot to quarantine, dumps and drops tables that are not in the snapshot, replays every row inside a database transaction with each write checked, and rolls the database back with the exact MySQL error if anything fails — so a restore can no longer leave the site with an empty options table.
* Live progress — snapshot create, snapshot restore and integrity scans show a progress bar the moment you press the button, then fill in with stage, items processed and percentage while the operation runs.
* Total protection score — one honest number out of 100, shown on the Dashboard and in the header badge on every screen. Each security setting is worth a fixed number of points and only earns them once it is verified active, so the score drops the moment a layer stops working. The Dashboard lists every setting that is still pending, how many points it is worth, and which tab fixes it.
* Setup tour — the first time you open the plugin, a guided walkthrough takes you through the tabs in order: Diagnostics, Set protection level, Snapshot, then the rest of the configuration screens.
* Clear layout — every screen is grouped into Everyday, Configure, Check and Incident, with a live protection badge, plain-language explanations of what each button does, and destructive actions visually separated from safe ones.
* Recoverable — a forgotten master password no longer locks you out of the settings. Reset it from the plugin's runtime directory in uploads with cPanel/FTP (create emergency-reset.key, then open emergency-reset.php), or from the shell with wp atvws password reset. Both routes sign out every session and are logged. WordPress account passwords can only be reset from WP-CLI. Email-code settings can be switched off from the shell with wp atvws otp disable.

Protection tiers:

* MEDIUM and STRONG both apply the same WordPress-level layers: WordPress, REST, AJAX, XML-RPC, upload, PHP shielding, integrity, CLI and cryptomining defence. Neither tier touches database grants or file permissions.
* LIMITED — protection is on but a core layer is switched off or did not verify after activation; the dashboard table shows which one before you rely on it.

Database and filesystem write protection are separate manual switches on the Dashboard. No tier turns them on, and they stay exactly as you left them when you change tiers. Both require protection to be on:

* Database write lock — the database account's write grants are revoked where a privileged MySQL account is configured (the wizard or the ATVWS_DB_ADMIN_* constants), and the SQL firewall blocks write queries from PHP regardless. It reports partial when only the firewall is enforceable. The site's own GRANT authority is never consumed.
* Filesystem write lock — wp-config.php and .htaccess are made read-only, and those plus the plugin's signing key are marked immutable via chattr where available. Original permissions are snapshotted and restored when you switch it off. wp-admin, wp-includes, plugins and themes are deliberately left writable so WordPress can update; integrity monitoring and the new-file watch report changes there. On a host where PHP cannot change file permissions the switch refuses cleanly instead of pretending to enforce it.

Both locks change the server itself, so neither is ever applied as a side effect of choosing a tier or enabling protection.

Choosing or changing a tier requires the master password, so a hijacked admin session cannot quietly weaken the site. Every layer also has its own switch for fine-tuning a tier; picking a new tier resets those switches. The old names STANDARD, AUTO and HARDENED still work and map to Medium and Strong.

Allowed writes:

Persistent visitor flows (comments, contact forms, WooCommerce, user registration) are BLOCKED by default. The owner re-enables only what the site genuinely needs from the "Allowed Writes" page. Everything else stays blocked.

Turning protection OFF is deliberately difficult (a strong request): you must submit a form, confirm a checkbox, type the exact phrase, and re-enter your account password (plus an optional passphrase). Emergency recovery paths (WP-CLI, a wp-config constant, a recovery file, or manual panel instructions) are documented to make sure you can always regain write access — see docs/RECOVERY.md.

== Installation ==

1. Upload the `atv-web-security` folder to `/wp-content/plugins/`.
2. Activate the plugin through the Plugins screen.
3. Under the new top-level "ATV Web Security" admin menu, follow the setup tour: review the Diagnostics tab, then click "Turn ON Protection".
4. Store the recovery token that is displayed once.
5. Configure "Allowed Writes", "Protected Admins" and "Two-Factor" to match your site.

More detail: docs/INSTALL.md and docs/CONFIGURATION.md.

== Frequently Asked Questions ==

= Can visitors still read the site? =
Yes. The site remains fully readable and functional for browsing. Only persistent changes are blocked.

= What happens while protection is ON? =
Users (including administrators) can browse, read and perform allowed writes. Plugin, theme, core updates, file and database changes that are not explicitly allowlisted are blocked. Turn protection OFF to build, update or modify anything.

= How do I turn protection OFF? =
Use the Dashboard tab and follow the strong-disable form, or use one of the emergency recovery methods described in docs/RECOVERY.md.

= Which password is accepted to turn protection OFF? =
The master password (8-64 letters, digits and symbols) is accepted, and so is your WordPress account password as a fallback — so a site that never configured a master password is never locked out.

= How does the emailed verification code work? =
When enabled, administrators are asked for a six-digit code sent to their email address after their password is accepted. The code field appears on the login form itself, so no page refresh is needed and every step stays on one screen. A code is valid for 10 minutes, can be resent once a minute (five codes per quarter hour), and the account locks for 15 minutes after five wrong entries.

= What if I cannot receive the emailed code? =
The login stays blocked on purpose rather than silently downgrading to password-only. Recover from the shell with wp atvws otp disable (or re-enter the master password on an already-unlocked session).

= Passkey or email code — which do I need? =
Either factor alone satisfies the check. An account covered by a registered passkey skips the emailed code, and the passkey is hardware-bound so it survives cookie clearing and cannot be copied. Use the two-factor email code when a browser cannot do WebAuthn.

= Do I need a passkey, or is the device token enough? =
The device token is bound to the browser profile: clearing cookies or switching browsers unlinks it, and a copied cookie could be replayed. A passkey is bound to the device's secure hardware, so it survives cookie clearing and cannot be copied. If you can only pick one, pick the passkey; keep the token as the fallback for browsers that cannot do WebAuthn. Enforcement is off until you turn it on, and no account is ever blocked without a registered passkey.

= What does the Miners tab do? =
It reports cryptomining findings without touching anything: script tags naming mining services or pools are removed from rendered pages, WordPress's HTTP API is refused for pool domains, a filesystem scan flags native executables and miner configuration markers, and a database scan looks for injected script blocks. Every result is surfaced on the Miners tab for you to review and act on.

= How does the vulnerability checker decide what is abandoned? =
It measures locally, with no third-party call: a release older than two years is flagged as unmaintained, a plugin or theme still on disk but no longer listed in the wordpress.org API is flagged as closed, and the local modification time of the plugin directory cross-checks both. An online feed lookup exists but is opt-in and requires your own API key — nothing phones home about the site by default.

= Is my database really read-only? =
Only when you switch the manual database write lock on. With a privileged MySQL account configured it revokes the database account's write privileges and verifies the result; without one it still blocks write queries from PHP through the SQL firewall and reports the lock as partial. No tier turns this lock on. Details in docs/INSTALL.md and docs/SECURITY_LIMITATIONS.md.

= What about caching plugins? =
Cache plugins are confined to an explicit allowlist of cache directories; other paths stay read-only. See diagnostics for the ongoing compatibility status.

= What does a restore need before it will run? =
Protection OFF, and both manual write locks (Database and Filesystem) OFF. The Database lock's SQL firewall refuses the DROP and CREATE statements a restore needs, and the Filesystem lock makes wp-config.php and .htaccess read-only, so a restore under either lock would silently do half its job — the Snapshot tab tells you which switch is still on. The snapshot is also verified first: a damaged one, one missing a dump of any live WordPress core table, or one without the schema needed to recreate a missing table is refused before anything changes.

= Can a restore break my site? =
Every database write is checked and the whole row replay runs inside a transaction (InnoDB), so a failed restore rolls back to the previous state and shows the exact MySQL error instead of leaving half-restored tables. Files that are not in the snapshot are moved to the plugin's quarantine folder, never destroyed immediately. The restore runs while the site is live, so a visitor can write a row mid-restore; the snapshot's copy wins that collision, and restoring during a quiet period gives the cleanest result. Snapshots made before version 1.0.48 lack the real schema files — restore only from snapshots taken with 1.0.48 or later.

= Why does WordPress say "One or more database tables are unavailable"? =
That message appears when core tables exist but the options table cannot supply a non-empty siteurl, and the built-in repair page cannot fix it because it never inserts missing rows. With 1.0.48+ a restore verifies siteurl before committing, so it cannot cause this state anymore. If you see it anyway, docs/RECOVERY.md has the exact SQL to restore siteurl and home through phpMyAdmin.

== External services ==

This plugin can connect to the AbuseIPDB API. This is optional and off by default; it is used only for the IP intelligence features (the site-wide IP reputation block and the login country check), and only when you switch those features on.

What the service is and what it is used for:

* AbuseIPDB provides an IP reputation and geolocation look-up. The plugin sends an IP address to AbuseIPDB to learn whether that address has been reported for abusive behaviour (a reputation score) and which country it is in, so the plugin can block abusive visitors or restrict administrator logins by country. No result is required for the site to work: if the lookup fails, the request is allowed (fail-open) and the failure is logged.

What data is sent and when:

* The visitor's IP address is sent to AbuseIPDB only at the moment a protected request is checked by one of the IP intelligence features above, together with your own AbuseIPDB API key (in the request header) needed to authenticate the lookup.
* No other data is sent. No personal data, no site content, no account information and no usage statistics leave the site. Requests are rate-limited, cached, and only made while the feature is enabled.

This service is provided by "AbuseIPDB LLC". Terms of use, privacy policy.

* Terms of use: https://www.abuseipdb.com/legal
* Privacy policy: https://www.abuseipdb.com/privacy

== Changelog ==

= 1.0.49 =
* Fixed the new-file alert overlapping the Cause/Action columns on the Dashboard and Integrity tabs: file paths in alert tables contain no spaces, so they could not wrap and stretched the table. Paths now break inside their own cell, including in the wp-admin notice and the WordPress dashboard widget.

= 1.0.48 =
* Fixed snapshot restore leaving the site with "One or more database tables are unavailable. The database may need to be repaired." Every table is now emptied and refilled inside a transaction (InnoDB), every inserted row is checked, the restored options table must contain a non-empty siteurl, and any failure rolls the database back instead of being reported as a successful restore. The repair page could never fix this because the missing row was siteurl itself.
* Snapshots now store the real CREATE TABLE statement for every table. The previous code wrote only the table name into each schema file, so a table that had been deleted could never be recreated.
* Snapshot creation now refuses to save an incomplete backup: it verifies that every row dump decodes, that the row counts match, and that siteurl is present.
* Extra files are now actually removed during a restore. The whole web root (not just .php/.txt) and wp-content drop-ins are covered, anything not in the snapshot is moved to the quarantine folder, and files that cannot be moved are reported instead of silently skipped.
* Extra database tables are dumped to a restore-audit folder and then dropped, with each DROP checked. Tables present in the snapshot but missing from the site are recreated from the stored schema.
* Restore is refused while protection or the Database/Filesystem write lock is on. Those locks were silently blocking the DROP/CREATE and file writes a restore needs, which is why extras survived and tables came back empty. Turn both off before restoring.

= 1.0.47 =
* Dashboard layout pass to match the approved design: a wider left column, roomier cards and table rows, pastel hero stat tiles, card-style pending items with numbered circles, and the Save menu setting button moved into the Plugins menu card header.
* Integrity now sits on the second navigation row beside Miners, Intrusions, Security Tests, Security Logs and Recovery.

= 1.0.46 =
* Each unexplained file alert now has a direct Delete button beside Dismiss, on the Dashboard card and the Integrity tab. Delete removes the file from disk after an explicit confirmation, then clears the alert and its snapshot entry so a file recreated at the same path is reported again. Only a path that still matches a stored alert and sits inside the WordPress installation can be deleted.
* Rearranged the Dashboard into two widget columns: the score, file watch, points, protection summary and layer table on the left; the Plugins menu, turn-off card, pending items, quick actions, tier chooser and recent activity on the right.
* New-file and new-table action rows keep their buttons aligned and wrap cleanly on narrow screens.

= 1.0.45 =
* New-file and new-table notifications now report only changes that nothing explains. Files and tables added by a plugin, theme, WordPress core or translation update stay listed on the Integrity tab with their cause, but no longer trigger dashboard notices or emails.
* A plugin or theme installed or updated outside WordPress (FTP, cPanel, a host-side updater) is now recognised by comparing plugin, theme and core versions between scans, so its files are not reported as unexplained.
* Alerts recorded before an update event existed are reclassified once the event arrives, and when this version first runs, existing alerts that live inside an installed plugin or theme directory are marked as explained.
* Email digests list only unexplained paths and state how many other files or tables were explained by an update.
* A bulk plugin update can no longer push an unexplained file out of the bounded alert list.

= 1.0.44 =
* Added a new-table watch alongside the new-file watch. Each scan compares the database table list with a signed baseline and reports tables that were not there before.
* Every new table is classified: a WordPress core table, created by a recent plugin or theme update, matching an installed plugin, or not explained. Unexplained tables are also recorded as DETECTED in the Intrusion log.
* Table alerts appear in the dashboard widget, on the plugin dashboard and on the Integrity tab, and are emailed in their own digest (capped at 100 tables).
* File and table notifications now have a Dismiss button per row, plus Dismiss all, on the dashboard, the WordPress dashboard widget and the Integrity tab.
* The first scan after updating records the table baseline, so tables already present are not reported.

= 1.0.43 =
* Email delivery: notification messages now use a sender on the site's own domain and set the administrator address as Reply-To, so shared-host mail filters stop rejecting them.
* A failed send is no longer silent. The reason reported by the mailer is stored, written to the log, and shown on the Dashboard and Integrity tabs next to the last attempt. The Send test email button now reports that same reason.
* Added a Dashboard switch for the Plugins menu. The Plugins screen can stay visible while protection is on, for reference. Plugin activation, deactivation, updates, deletion, installs and the code editors remain blocked.
* Bulk plugin actions (activate, deactivate, delete, update selected) and their action2 variants are now blocked explicitly.
* When the Plugins screen is visible, its per-plugin Activate/Deactivate/Delete/Edit links are removed so the screen offers no action that ends in a block page.
* New installations default to a 15-minute scan interval; existing installations keep their saved value.

= 1.0.42 =
* Fixed new-file alerts appearing to do nothing. The watch now records its baseline at activation instead of on the first admin page view, runs every five minutes through WP-Cron with a configurable interval, and rescans when the plugin screen is opened (at most once a minute).
* The plugin dashboard now always shows the new-file watch state: last scan, next automatic scan, files watched, email recipients, whether the last email was delivered, and a Scan now button. The WordPress dashboard widget also has Scan now.
* Findings are shown as a wp-admin notice on every admin screen, not only inside the plugin page.
* Added a Send test email button and a visible last-email result so a broken server mail setup no longer looks like "nothing was found".
* Reduced automatic scan interval from one hour to 15 minutes (selectable from 5 minutes to 6 hours) and replaced the hourly cron event with a five-minute recurrence.
* A watch that cannot write its snapshot now logs a CRITICAL entry instead of silently re-baselining on every scan.

= 1.0.41 =
* Fixed the plugin page opening blank on a fresh install. The first-run tour is now drawn in place instead of redirecting: WordPress has already sent the admin header when a menu callback runs, so the redirect could not be issued and ended the request with an empty content area.
* A page that fails while rendering now shows the error and records it in the plugin log instead of ending blank.
* Hardened the protected-administrator list against a malformed legacy value that would have failed every admin page before it could render.

= 1.0.40 =
* Added the new-file watch: the whole WordPress installation is scanned on a schedule, and every file that was not present before is reported with its full path, detection time and size, on the dashboard and by email.
* Each new-file alert states whether a WordPress core, plugin, theme or translation update recorded by the upgrader explains it, or whether no update does.
* Database and filesystem write protection are now manual Dashboard switches. No protection tier turns either of them on, and changing tiers leaves them exactly as they were.
* The database write lock is always available and reports partial when only the SQL firewall can be enforced; the filesystem lock refuses cleanly on hosts that cannot change file permissions.
* The protection score, status banner and documentation now reflect the manual locks; integrity points require a verified baseline.

= 1.0.37 =
* Maintenance release addressing WordPress.org plugin review feedback. No feature or behaviour changes.
* Admin stylesheet is now loaded with wp_enqueue_style instead of a printed raw style block.
* Theme and plugin headers are read with get_file_data() instead of file_get_contents().
* The AbuseIPDB external service is now documented, with links to its terms and privacy policy.
* Runtime, state and recovery paths now use wp_upload_dir() and WPMU_PLUGIN_DIR for custom and multisite layouts.
* Removed the global automatic-update filters; automatic updates are left to WordPress.
* Removed the automatic mu-plugin installer; the optional MU bootstrap is a manual copy.
* The miner-guard output buffer is now always closed.
* The top-level admin menu is placed lower (position 100).
* Remaining request inputs are sanitised and validated; output is escaped.

= 1.0.36 =
* Email verification code now appears as an inline field on the login form — no page refresh, the whole two-factor flow stays on one screen.
* Resend is throttled to once a minute and five codes per quarter hour; a failed delivery keeps the login blocked on purpose so the mailer cannot fail open.
* Codes are probed in the background on submit, with a clear error on the form when the code is wrong.

= 1.0.35 =
* Added email verification codes (Two-Factor): administrators can be required to enter a six-digit code emailed to them after the account password. Passkey-covered accounts skip the code — either factor alone satisfies the login.

= 1.0.34 =
* Allowed writes now defer to the SQL firewall's enforced rule server-side, so a server-level write block is verified against the same rule that actually runs.

= 1.0.0 =
* Initial release.

== Upgrade Notice ==

= 1.0.49 =
* Stops long file paths in the new-file alerts from overlapping the neighbouring columns on the Dashboard and Integrity tabs. Recommended update.

= 1.0.48 =
* Fixes snapshot restore leaving the database without a siteurl (WordPress then shows "One or more database tables are unavailable") and failing to remove extra files and tables. Restore now verifies every row and rolls back on failure, and it must be run with protection and both write locks off. Create a fresh snapshot after updating. Recommended update.

= 1.0.47 =
* Polishes the Dashboard layout and spacing to match the approved design, and moves Integrity to the second navigation row. No functional changes. Recommended update.

= 1.0.46 =
* Adds a Delete button to each unexplained file alert and rearranges the Dashboard into two balanced widget columns. Delete asks for confirmation and only touches a file that still matches a stored alert inside the WordPress installation. Recommended update.

= 1.0.45 =
* Stops legitimate plugin, theme and WordPress updates from appearing in new-file notifications. Only files and tables that no update or installed plugin explains are reported; explained changes remain visible on the Integrity tab. Recommended update.

= 1.0.44 =
* Adds detection of new database tables with the same update-aware classification and email/dashboard alerts as new files, plus a Dismiss button on each notification. The first scan records the baseline; existing tables are not reported. Recommended update.

= 1.0.43 =
* Fixes new-file notification emails not arriving: site-domain sender, Reply-To, and a visible mailer error on failure. Adds a Dashboard option to keep the Plugins menu visible while protection stays enforced. Recommended update.

= 1.0.42 =
* Makes new-file detection prompt and visible: five-minute cron, a scan when the plugin screen opens, always-visible watch status, a Scan now button, admin-wide notices and a test-email control. Recommended update.

= 1.0.41 =
* Fixes the plugin page opening blank on a fresh install. Updating is safe; settings and protection state are untouched.

= 1.0.40 =
* Adds new-file detection with email and dashboard alerts, and moves database/filesystem write protection to manual switches. Database grants and file permissions are no longer changed by choosing a tier.

= 1.0.37 =
* Maintenance release for WordPress.org review compliance. No behaviour changes; updating is safe.
* If you already use the two-factor email code, nothing changes: the code field stays inline on the login form.