== Changelog == = 1.0.147 = * Detection now understands a function or class name written as an EXPRESSION, not only as a plain word. Until now the scanner recognised `Closure::fromCallable("system")` but not the identical attack written as `$c = "Closure"; $c::fromCallable("system")`, and it recognised `new ReflectionFunction("system")` but not `$rf = "ReflectionFunction"; new $rf("system")`. A single shared resolver now works out the text a name expression produces - a variable, two pieces joined together, a name in brackets, a name in a heredoc, `X::class`, or a name produced by strrev()/base64_decode()/str_rot13() - and every check consults it. Thirteen ways of writing the same backdoor, all previously invisible, are now reported. * Fixed a serious blind spot found while mapping the above: `new ReflectionFunction( $_POST['fn'] )` - a backdoor that lets the visitor choose which function to run - was silenced by its own square brackets, because the scanner mistook a normal array index for a "safe" bounded callable. The same code written with an intermediate variable was correctly reported, so the scanner was contradicting itself. * Fixed a second blind spot from the same mapping: a single pair of brackets around a call - `($f)($_POST['a'])` - hid every callable-factory detection in the product, including ones added in earlier releases. Until this was closed, each of those detections was one character away from being bypassed. * Also closed: a plain piece of text used directly as a function - `"system"($_POST['c'])` - which is valid PHP and the shortest possible request-controlled backdoor, and was not reported by any earlier version. * False alarms found by our own adversarial review of the changes above, before release. Ordinary plugin code that stores the RESULT of call_user_func() and then calls it - the standard router / hook-resolver pattern - was briefly reported as critical malware, as was a vendor class name held via `::class` (webmozart/assert, one of the most widely bundled libraries) and a plain user-defined constant. All are correct again, and each has a permanent test guarding it. * Verified on 6,637 real files: no new findings anywhere, every known-malware test file still detected, and 34 new test cases covering exactly these shapes (the previous version fails 20 of them). = 1.0.146 = * The scanner now understands how PHP resolves FUNCTION names, not just class names. Inside a namespace an unqualified call can mean a function of your own, a function imported with "use function", or - only when neither applies - PHP's global function. Getting this wrong was wrong in both directions: your own namespaced function named system() or exec() called with request data was reported as CRITICAL MALWARE, while a real backdoor written as `use function \system as run; run($_POST['c']);` was not reported at all. Both are fixed, including grouped imports such as `use function Acme\Lib\{ system, exec };`. * Closed a bypass of the callable-factory check: creating the closure through a callback function - `call_user_func(["Closure","fromCallable"], "system")($_POST['a'])` - produced no finding at all, although the direct form has been reported as malware since 1.0.143. All spellings are now covered, including the form that stores the result in a variable first. A class of your own named Closure is still not reported. * Detection hardening found by our own adversarial review of the changes above, before release. Recognising a locally declared function as "shadowing" the global one initially made it possible to silence the scanner with a single line of code that never runs - `if (false) { function system($x){} }` - and likewise with a declaration hidden inside another function, inside a closure, or in PHP's alternative if/endif syntax. A declaration is now only treated as shadowing when it is unconditional and at the top level of a real namespace; when it is ambiguous the scanner reports rather than stays silent. A class name written as text is also now treated the way PHP treats it - as fully qualified - so a namespace declaration can no longer suppress the check. * Verified on 6,637 real files: no new findings anywhere, every known-malware test file still detected, and 26 new test cases covering exactly these shapes (the previous version fails 11 of them). = 1.0.145 = * Finished removing the false alarm that 1.0.144 only narrowed. PHP allows spaces and line breaks around the `->` and `::` operators, so `$pdo -> exec( $_POST['table'] )` was still reported as CRITICAL MALWARE even after the previous fix - as were `Foo :: exec(...)`, `$this -> system(...)` and every other method whose name matches a command function. The eight text-matching patterns behind this have now been RETIRED entirely and the job handed to the code-structure analyser, which reads actual PHP tokens: it does not care about spacing, it knows a method call from a global function call, and it ignores comments and text. Two additional false alarms disappear with them: a file that merely MENTIONS these functions in a comment or in a string is no longer reported (in the previous version, a documentation file explaining what not to do raised eight critical malware findings). * No detection is lost, and some is gained. Verified on 6,633 real files plus seven purpose-written fixtures: every known-malware test file is still detected, the previous version raised 22 critical-malware findings on that clean legitimate code and this version raises none, with zero new findings anywhere. The analyser now also catches these commands when written with a leading backslash, when the command is built by joining text, when it passes through a sanitiser, and when it comes from the raw request body. * Closed a bypass: writing a leading backslash before a callback function - `\call_user_func("system", $_POST['a'])`, `\array_map(...)`, `\array_walk(...)`, `\usort(...)` - hid a backdoor from the scanner completely. It is the same global function either way, and all forms are now detected. * Closed a detection gap in the callable-factory checks: a dangerous function name held in a VARIABLE - `$f = "system"; Closure::fromCallable($f)($_POST['a'])` - and a function chosen by the request itself - `$f = $_POST['f']; (new ReflectionFunction($f))->invoke($_POST['a'])` - were not reported, although the identical forms written literally were. Both are now reported as malware. A harmless name such as strlen stays clean, and reassigning the variable still clears the finding. * Signature database version 2.21.0 -> 2.22.0. = 1.0.144 = * Removed a serious false alarm affecting ordinary database and process code. Eight patterns that look for a command being run on data from the web request were also matching a METHOD of the same name on one of your own objects. So `$pdo->exec( $_POST['table'] )` - which is simply how PDO runs a database statement - was reported as CRITICAL MALWARE, and so was any object or class with a method named exec, system, shell_exec, passthru, popen, proc_open, pcntl_exec or create_function called with request data. Worse, these particular patterns were marked as never-downgradable, so not even the known-library allowance could soften them: a legitimate plugin using PDO could show a red "critical malware" result on a clean site. The patterns now ignore a call made through an object (`->`) or a class (`::`) and match only a real global function call. * Nothing is lost from detection. The genuinely dangerous form - calling the global function directly on request data - still matches, and it is independently confirmed by the code-structure analyser, which understands the difference between a method and a global function. Verified against a corpus of 6,633 real files plus three new fixtures written for exactly this shape: the previous version reported twelve critical-malware findings on that clean, legitimate code and this version reports none, with zero new findings anywhere else and all known-malware test files still detected. * Signature database version 2.20.0 -> 2.21.0, which refreshes results for files that were already scanned. = 1.0.143 = * Detection: the SIMPLEST form of a command-execution backdoor is now reported as malware instead of as an item to review. Code that stores a dangerous function's name in a plain piece of text and then calls it - for example putting "system" in a variable and running it on data from the request - was only ever a review item, while far more exotic disguises of the exact same attack were already reported as malware. Every spelling of it is now covered: the name written inline, split across two pieces of text, with a leading backslash, wrapped in one redundant pair of brackets, or passed through a callback function such as call_user_func or array_map. * Detection: the scanner now resolves class names the way PHP itself does. Previously it compared the text of a name, which was wrong in both directions at once - a class of your own named Closure or ReflectionFunction inside your own namespace could have its code reported as malware, while a genuine backdoor written with a fully-qualified name was missed entirely. Namespaces, imports (including grouped and renamed imports), and the namespace operator are all now understood. * Detection: closed three ways an attacker could have hidden from the previous build's new checks, all found by our own adversarial review before release. The most serious: a single line of ordinary-looking code placed anywhere in a file could switch off the entire family of checks that catch a wrapped command-execution backdoor. * False alarms removed, all found by our own adversarial review of the changes above. None of these ever reached a released build. Ordinary plugin code that compares a value against a name ("if the mode is 'exec' do this, otherwise do that"), code that builds a different function's name by joining text together, a loop that reuses a variable, and creating an object whose class name is held in a variable are no longer reported as malware. Two long-standing internal bugs surfaced by the same review are also fixed: text containing an embedded value (for example "user-{$id}") made the scanner lose track of where a function began and end up examining the wrong variables, and a declaration without a body (as in an interface) had the same effect. * False alarms removed on legitimate code, verified against a corpus of 6,633 real files: WordPress core, Contact Form 7 and Elementor each had a review item raised by a rule that did not apply the safeguard it was supposed to. Calling a fixed method on a known object can never reach a dangerous global function, and is no longer reported at any level. Compared with the previous release this version adds ZERO new findings on that corpus and removes four. = 1.0.142 = * Accuracy fixes to the callable checks added in 1.0.141 (which was never released), all found by our own adversarial review: code that merely PASSES a function reference to another function is no longer mistaken for running it; a class of your own (or a plugin's) that happens to provide a method named "fromCallable" no longer causes its argument to be reported as malware; and two cases where a genuinely dangerous call was missed - one where an ordinary block of code appeared just before it, and one where it was written inside a print statement - are now detected. = 1.0.141 = * Closed a family of ways an attacker could hide a command-execution backdoor from the scanner. Previously the scanner recognised the plain form (for example passing "system" to a callback function together with data from the request) but missed the same attack written differently: with a leading backslash ("\system"), with the name split in two ("sys" . "tem"), wrapped in a callable factory (Closure::fromCallable or ReflectionFunction), invoked through the closure object instead of directly, or written as a PHP 8.1 first-class callable. All of these run the same command on the same request data, and all are now reported as malware. The check that decides whether a name is a dangerous function is now defined in ONE place and used by every path, so a new wrapper costs one line instead of a new blind spot. * Kept the safeguards that prevent false alarms: calling a METHOD that merely shares a name with a dangerous function (for example an "exec" method on your own object, $this->exec(...), or Foo::exec(...)) is still not reported, and neither is a harmless function such as strlen passed the same way. * Fixed false positives found in our own adversarial review of the above: an ordinary method named "call" or "__invoke" on an object (WordPress core itself does this) is no longer reported; a function name assembled from unrelated pieces of text is no longer mistaken for a dangerous one; and a namespaced function call that merely RECEIVES a closure as an argument is no longer reported as invoking it. = 1.0.140 = * Fixed a false positive introduced in 1.0.139 (which was never released): a rule meant to catch the WP-VCD backdoor loader matched the filename anywhere in a file's text, so a code COMMENT describing the malware, or an ordinary sentence such as "to install, include wp-feed.php manually", was reported as critical malware. The check now understands PHP code structure - it reads the actual include/require statement and ignores comments and text, so only a real loader is reported. It also now catches loader forms it previously missed (a path built with a variable inside quotes, a heredoc path, or a filename split across two pieces of text), and it no longer reports unrelated files such as a "wp-feed.php.html" cache fragment. WordPress's own wp-includes/feed.php is never reported. * Closed a detection gap: code that wrapped a dangerous function in a callable factory - for example Closure::fromCallable("system") called with data from the request - was not detected at all. It is now reported as malware, in both the immediate form and the form that stores it in a variable first. Calling a method that merely shares a name with a sanitiser, or with a harmless function, is still not reported. * Detection accuracy (found in our own adversarial review of the above): the new dangerous-callable tracking was not limited to the function it appeared in, so a harmless use in one function could cause an unrelated variable of the same name elsewhere to be reported as malware. It is now scoped per function, cleared when the variable is reassigned, and produces no finding at all when the call carries no request data. = 1.0.139 = * Fixed a false positive that could report a completely clean WordPress as infected. Two core files that merely MENTION the filename "wp-feed.php" (a legacy redirect entry in wp-includes/canonical.php and a code comment in wp-admin/includes/update-core.php) were flagged as a critical backdoor, because a rule about a file's NAME was being matched against file CONTENT. The scanner now supports name-based rules and matches the actual filename; a separate rule still catches the real WP-VCD loader that injects wp-feed.php. Signature DB version 2.18.0 -> 2.19.0 (invalidates the verdict cache). Clean corpus: the two core files are no longer flagged and no new false positives appear; all known-malware fixtures still detected. * Fixed a related false positive on two WordPress admin files (wp-admin/widgets-form.php, wp-admin/includes/ajax-actions.php) whose legitimate widget/AJAX dispatch was reported as a critical dynamic-call backdoor. The malware model now distinguishes code whose callable comes DIRECTLY from the request (still reported as malware) from a value read out of a developer-defined table via a request-controlled key (reported as an item to review). Direct and one-hop request-controlled calls remain confirmed malware. * Detection hardening (found in our own adversarial review): a value passed through a method that happens to share a name with a sanitiser function (e.g. a custom intval() method) is no longer treated as sanitised, so a dynamic call built from it is still detected. * The scan result card and the results panel now describe the same scan the same way when a site has both unrecognised core files and serious review items — they list them in the same priority order, so the headline can never disagree with the details. * If your file-integrity baseline was created before large-file tracking was added, the plugin now shows a one-time reminder to rebuild it so files over 50 MB are also monitored for tampering (previously this was only mentioned in the changelog). * Removed unused internal fields from the scan summary that were never shown and contained an unfinished, untranslated placeholder. = 1.0.137 = * Two-factor sign-in no longer locks you out of your own site: the "now enter your code" step (password already correct) and mistyped codes were counted as failed logins and could lock your own IP for 30 minutes. Second-factor states are no longer counted; wrong passwords still are, and two-factor keeps its own separate limit. * Quarantine & Safety Snapshots survive regenerating WordPress keys/salts and site clones/migrations: records were tied to a rotatable salt, so the list went EMPTY, new quarantines were refused and restore/WP-CLI recovery failed. Now tied to a dedicated key. Older records are accepted on read and rewrapped on their next successful save; records whose salt was ALREADY rotated before this version first ran cannot be recovered (the original secret no longer exists) - documented limitation. Tampered records are still rejected. * "Analyze with AI" surfaces the real server status (e.g. HTTP 502) instead of reporting "signature mismatch" for every outage. Tampered responses are still rejected. * File-integrity scan / baseline no longer wedge on a file over 50 MB (endless retry of the same file). Such files are skipped and reported in their own count, so an ordinary backup/media file never marks a healthy site "incomplete". * A scan that skipped a file while running can no longer report a clean all-clear; it now reports incomplete coverage. A genuinely clean scan is unchanged. * MalwareBazaar lookups return "not known" on a malformed reply instead of "Confirmed Malware, risk 100"; third-party feed fields are sanitised at ingest. * DETECTION change (widened): hex-obfuscation signature made possessive — a matching run of ~60 KB+ previously hit the PCRE JIT stack limit and returned FALSE, which callers read as "no match" (silent false-negative). Counts for this signature alone: no change to the known-malware fixture verdicts; clean 19,916-file corpus 33 -> 33 findings (zero new false positives); matches now succeed at 5 KB / 60 KB / 120 KB with no JIT error. Signature DB version 2.17.0 -> 2.18.0 to invalidate the verdict cache. Severity/category unchanged. * Governance gate: the last ungated site-source hash (an approved-file re-check that could follow a symlink) is now behind the shared no-follow contract — tools/check-no-follow.php reports 0 ungated call-sites. * Quarantine "empty the file" path now pins file identity (lstat/fstat dev+ino on the open handle) before truncating, and refuses when identity cannot be proven, so a symlink swapped in at that instant can never redirect the truncate to another file. * Scan-start failure message now names both real causes (folder not writable, or a filesystem without flock support) instead of blaming permissions only. * Store-key safety (found in review): if the dedicated key could not be SAVED to the database (DB error, read-only replica, disk full), the previous build still wrapped records with a key that existed only for that one request - save reported success and the record was unreadable forever after. The plugin now refuses to write and reports a real error instead. * Salt pinning (found in review): the auth salt in use is recorded the first time this version touches the store, so records written by an older version are still readable even if you regenerate your keys BEFORE they have been re-secured. (Records whose keys were already regenerated before this update cannot be recovered - the original secret no longer exists.) * A file with high-risk executable behaviour (e.g. a function name AND its argument both taken straight from the request) no longer sits under a green "No malware found" headline; the scan reports amber "issues to review". Sites with only ordinary review items from legitimate plugins still show the normal green result. * Privacy notice corrected: the plugin does NOT contact URLhaus and never sent any URL from your files. The claim has been removed from the privacy notice and readme rather than left inaccurate. * Baseline now tells you when a file was too large to index: the confirmation says "N files were larger than the 50 MB limit, so their contents are not covered by change monitoring" instead of a green tick that hides them. The scan status banner states the same fact. * Over-size files are now RECORDED in the baseline instead of being left out of it. Three consequences: a monitored file that is padded past the 50 MB limit is reported as changed (previously it silently dropped out of change monitoring entirely, with no signal at all); an over-size file that later shrinks below the limit is reported as changed rather than as a bogus "new file"; and deleting an over-size file is now noticed. These three take effect once you create or refresh your baseline — an existing baseline has no record of its over-size files, so it cannot report on them until it is rebuilt. * A manual scan and a scheduled scan now agree about the same file. Both ask one shared definition, and an ordinary backup or media file over 50 MB no longer marks a healthy site "incomplete" on EITHER path — previously a scheduled scan turned amber and told you the file "could not be read" and to re-run the scan, which was untrue (the file reads fine, it is simply large) and could never be cleared. Files we genuinely could not read still mark the scan incomplete. * Fixed: after the scanner discarded findings for files that no longer exist, it recomputed the summary WITHOUT the coverage counters — which could turn a scan green ("no malware") even though some files were never inspected. * Scan results now say what actually happened in states that previously all shared one vague "Some issues detected": a serious-but-unconfirmed item, an unrecognised file in a WordPress core folder, and confirmed lower-severity malware each get their own headline. A scan that finds malware AND could not check part of your site now says both, instead of dropping the coverage warning. * DETECTION change (widened, nothing removed): a file that takes BOTH the function name AND its data straight from a web request (e.g. $f = $_POST['f']; $f($_POST['a']);) is now CONFIRMED MALWARE. It previously produced only review-level findings, so a textbook webshell was reported under a green "No malware found" headline with 0 threats. New tokenizer id tk_dyn_request_callee_var (critical). Deliberately narrow so the ACG-110 false-positive work is not undone: a request-derived callee ALONE stays a review item, and method/static dispatch ($this->$m(), Foo::$m()) stays a review item too - it can only reach a method that exists on that class, which is the validated-dispatcher shape, whereas a plain $f() can reach any php function including system(). Counts: known-malware fixtures 13/14 -> 14/14 (this is the authoritative end-state for this release); clean 19,916-file corpus 33 -> 33 findings, ZERO new false positives (a first, broader cut of this rule flagged Elementor's nonce+capability+in_array-guarded template dispatcher as critical malware and was rejected by the corpus run before shipping). * Store key is now created ONCE and never overwritten (found in review): two requests racing on first use could each generate a key, so one could save a record with K1 while the option already held K2 - leaving that record unreadable. Creation now uses an atomic add_option(); a request that loses the race adopts the winner's key, and if no key can be created or read the write is refused. * DETECTION change (widened, found in review): the same request-controlled dynamic call is now confirmed malware through the callback wrappers too - call_user_func($f, $_POST[..]), call_user_func_array(), array_map() and the other callable APIs - and also when the argument was passed through sanitize_text_field() or similar (which strips HTML but does nothing to make an argument safe for system()/exec()). Counts: fixtures 14/14; clean 19,916-file corpus 33 -> 33 findings, zero new false positives; validated dispatchers ($this->$m(), Foo::$m()) still classify as review. * A modified WordPress core file no longer produces a green "No malware found" result (found in review): the previous build exposed the flag but never consulted it, so is_clean stayed true and the advice read "common in legitimate plugins". It now reports the core-integrity issue and recommends reinstalling core. * The top status banner no longer says "You are protected / All clear" when there is a high-priority review item, a modified core file, or incomplete scan coverage - it has its own logic and was not honouring those states. * Store key creation moved to a real database-level create-once, INSERT IGNORE (found in review): the previous build used add_option(), which the WordPress 6.7.1 source shows is NOT create-if-absent - its existence test is a PHP-level get_option() and the write is INSERT ... ON DUPLICATE KEY UPDATE option_value = VALUES(option_value), so two requests racing on first use could still overwrite each other. The key is now inserted with INSERT IGNORE against the wp_options UNIQUE index and read back straight from the database (never the object cache), so the first writer wins and nothing can overwrite it. * DETECTION change (widened, found in review): request data that reached a callback API through a VARIABLE is now recognised. call_user_func("system", $a) where $a came from the request was reported CLEAN; array_map("system", $a) and call_user_func_array("system", [$a]) likewise; and call_user_func($f, $a) with both sides attacker-controlled was only a review item. All are now confirmed malware - the wrapper has exactly the capability of the direct call. Counts: fixtures 14/14; clean 19,916-file corpus 33 -> 33 findings, zero new false positives. Object/array callables (array($this,"m"), [$app,$method], $this->table[$k]["callback"]) and closure literals stay review: they can only reach a method the developer defined, and two clean-corpus runs rejected earlier, broader cuts of this rule that flagged Contact Form 7 and Elementor. = 1.0.88 = * Core integrity (symbolic-link core folder, offline too): a wp-admin / wp-includes folder that has been replaced by a symbolic link is now reported as high-severity suspicious and is NOT walked — even when the official WordPress checksums cannot be downloaded (offline). The deep core walker also no longer reads a server-config file through a symbolically-linked core folder. * Quarantine & Safety Snapshots restore (container/overlay filesystems): restoring a file no longer fails on overlay/container filesystems (e.g. Docker) where a file and its directory report different device IDs even though a hard-link publish works. The restore now attempts the atomic no-clobber link directly (it writes nothing on failure, so a partial live file is still impossible) and only fails on a genuine cross-device error. * Build-time governance gate upgraded: tools/check-no-follow.php is now call-site/dominance-aware — it requires a no-follow guard on the SAME file path immediately before each read/hash, instead of merely somewhere in the function, so a single ungated read can no longer hide next to a guarded one. * No change for normal sites: every genuine file is scanned, verified and restored exactly as before. = 1.0.87 = * Symbolic-link / special-file safety completed at the last few internal read/hash sites and locked in by a build-time check: * Own-plugin self-integrity: a plugin file reachable only through a symbolic-link directory (or a special file) is no longer trusted as "unmodified" — it is scanned — and a named pipe can no longer stall that check. * Ignore list: deciding a file is "ignored and unchanged" now refuses a symbolic-link path or special file before hashing, so an attacker cannot keep a file permanently out of scan by swapping a parent folder to a symbolic link, and a named pipe can no longer stall the ignore check. * AI prompt excerpts: the code excerpt sent for AI analysis now refuses a symbolic-link path or special file, so external file content never reaches the prompt and a named pipe can never stall analysis. * New build-time governance gate (tools/check-no-follow.php, CLI only): the release build now fails if any file-reading or hashing site that touches site files is not covered by the shared "regular file, no symbolic-link path component, inside the site" rule — so this protection can never silently regress. * No change for normal sites: every genuine file is scanned, ignored, analyzed and verified exactly as before. = 1.0.86 = * Symbolic-link safety completed across remediation, threat-lookup and admin reads (the same "regular file, no symbolic-link path component, inside the site" rule now used everywhere a file is read or hashed): * Quarantine & Safety Snapshots (validate-then-use hardening): the original file is RE-validated at the exact moment it is copied/hashed/removed — not only when the action starts. A parent folder swapped to a symbolic link AFTER validation can no longer cause a copy/snapshot of content from outside the site, and the original is never removed through a swapped link. * Malware-confirmation lookup (MalwareBazaar): refuses a symbolic-link path or a special file (named pipe) before hashing — so it never hashes content from outside the site and a special file can never stall a scan. * AI analysis & the admin "view file" tool: reject a special file (named pipe / device) before reading, returning a clear "not a readable regular file" message instead of hanging the request. * No change for normal sites: every genuine file is still quarantined, snapshotted, looked up, analyzed and viewed exactly as before; only symbolic-link / special-file paths are refused. = 1.0.85 = * Symbolic-link safety extended to the last three scan paths: the official "Verify Core Files" tool, the scheduled (automatic) file-integrity scan, and the baseline builder now ALL use the same shared "regular file, no symbolic-link path component, inside the site" rule before hashing — closing the same symbolic-link-parent / symbolic-link-core-directory gap that was already fixed in the malware and on-demand integrity scanners. * Verify Core Files: a core file that is a symbolic link, or that sits inside a symbolically-linked core folder (e.g. a wp-includes replaced by a link), is now reported as changed instead of being hashed "through" the link and marked verified; a symbolically-linked core folder is no longer walked as if it were genuine core. * Scheduled integrity scan & baseline builder: a file whose parent folder is swapped to a symbolic link after discovery is counted as a skipped coverage gap (so the scan reads incomplete and the baseline stays partial) instead of hashing content from outside the site; a named pipe / special file can no longer stall these scans. * No change for normal sites: every one of these still hashes all genuine files (a real install indexes thousands with zero skipped); hosts that legitimately publish wp-content or uploads as a symbolic link are unaffected — only links planted deeper inside are refused. = 1.0.84 = * Core integrity (symlinked-directory safe): the official core-file check now refuses a core file reachable only THROUGH a symbolic-link directory (for example a wp-includes folder replaced by a link to an external tree) — it is reported as suspicious instead of being hashed "through" the link and marked verified. Previously only a link on the file itself was caught, not a linked parent folder. * Malware scanner (symlinked-parent + special-file safe): a file whose parent directory was swapped to a symbolic link after the scan started is now counted as a skipped coverage gap instead of being read from outside the site; and a named pipe / special file is skipped up-front so it can never stall a scan. * File-integrity scan & baseline (symlinked-parent safe): the batch integrity scan and the baseline builder validate the WHOLE path (no symbolic-link component, resolves inside the site) immediately before hashing, so a folder swapped to a link after discovery can never index or hash content from outside the site. * One shared, host-aware safety rule: all of the above now use a single "regular file, no symbolic-link path component, inside the site" check. It deliberately still allows hosts that legitimately publish wp-content or the uploads folder as a symbolic link — those sites scan exactly as before; only links planted deeper inside are refused. = 1.0.83 = * Core integrity (symlink-safe): the official WordPress core-file verification now refuses a core file that has been replaced by a symbolic link or special file — it is reported as suspicious instead of being hashed "through" the link and marked verified. * Malware scanner (honest coverage): when the scanner refuses to read a file that became a symbolic link mid-scan, it now counts it as a skipped coverage gap instead of "verified clean". * File-integrity baseline (database fallback): the baseline now migrates a legacy database copy ONLY on a genuinely fresh install (its storage folder does not yet exist) — a folder that already exists is authoritative, so a folder deleted-and-recreated during a read can never cause a stale database copy to be trusted. * Pattern detector & visual diff (symlink-safe): both now refuse symbolic-link / out-of-install paths before reading, so external file content can never be scanned or shown. * Scan coverage accounting: excluded directories that are symbolic links are no longer counted as coverage gaps, so excluded folders never produce a false "incomplete" result. = 1.0.82 = * Scanner & baseline (symlink-safe everywhere): the malware scanner, the batch integrity scanner and the baseline builder now ALL skip a symbolic-link file as a coverage gap — at discovery AND again immediately before each file is read — so a file swapped to a symbolic link after discovery (a race) can never cause the link's target outside the WordPress install to be read, hashed or scanned. * File-integrity baseline (folder swapped mid-read): the baseline now detects if its storage folder is replaced by a symbolic link at ANY moment during a read (not just at the start) and fails closed, so a database copy can never be loaded behind a folder that was briefly swapped and restored. * File-integrity baseline (honest coverage): building the baseline now reports how many files were skipped (symbolic links / unreadable) instead of silently omitting them, and applies your folder exclusions before counting, so excluded folders never produce a false "incomplete" result. = 1.0.81 = * Storage hardening (baseline folder swapped mid-read): the file-integrity baseline now re-checks that its storage folder is still a real directory immediately before any database fallback, so a folder swapped to a symbolic link DURING a read can no longer cause a database copy to be loaded behind the link. * Snapshot deletion (symlink-safe): deleting a snapshot no longer follows a planted symbolic link inside the snapshots folder — a link is removed as a link, so a malicious link can never cause a sibling snapshot's files to be deleted. * Scanner & baseline (symlinked files): a symbolic-link FILE encountered while scanning or building the file-integrity baseline is now counted as a coverage gap and skipped, instead of following the link and hashing its target outside the WordPress install. This matches how the main malware scanner already treats symlinks. = 1.0.80 = * Storage hardening (symlinked storage folder): if the plugin's own data folder (wp-content/guardpress-data) or the file-integrity baseline folder (uploads/guardpress-baseline) — or one of their sub-folders — has been replaced by a symbolic link, the plugin now treats that storage as UNVERIFIABLE and FAILS CLOSED, instead of reading "through" the link and then falling back to a database copy. A tampered storage folder can no longer cause a manifest or baseline to be served from a lower legacy/database tier behind the link. * Internal: the storage presence check and the baseline loader now both validate that the storage root and sub-folder are real directories (not symlinks, and resolving inside the expected location) before deciding a file is "absent". = 1.0.79 = * Storage hardening (baseline existence consistency): the file-integrity baseline "exists" check and the loader now agree about the database fallback too — "exists" tests the same stored baseline value the loader would actually serve, instead of a separate count metadata option that could disagree (e.g. report a baseline exists when no baseline data would actually load). Found and fixed by an internal exhaustive state-matrix self-audit of the storage read/fallback paths. = 1.0.78 = * Storage hardening (corrupt-primary fail-closed): if a data, manifest or file-integrity baseline primary file is present but damaged/unreadable (does not decode), the plugin now FAILS CLOSED for that read instead of silently serving an older legacy copy or a legacy database value. The damaged file's legacy copy is kept on disk (never destroyed), so a later successful save restores it — but a damaged primary is never served as if it were current, trusted data. * Storage hardening (baseline fallback order): the file-integrity baseline only falls back to its legacy database record when BOTH on-disk baseline files are genuinely absent — never when the primary is tampered or damaged. * Storage hardening (consistent existence): the baseline "exists" check now follows the exact same fail-closed logic as the loader, so the two can never disagree. = 1.0.77 = * Storage hardening (fail-closed reads): when a data, manifest or file-integrity baseline file has been replaced by a symbolic link or special file, the plugin now FAILS CLOSED instead of silently falling back to an older legacy copy. A tampered (symlinked/special) primary file can no longer cause stale or attacker-substituted data to be loaded, and can no longer be reported as if it came from the trusted primary store. * Storage hardening (consistent existence): the file-integrity baseline "exists" check now agrees with the loader — a symbolic link or special file at the baseline name is treated as not-a-real-baseline rather than reported as present. * Internal: data reads now distinguish "absent" from "unsafe/unreadable", so a missing file still falls back to a legacy copy while a tampered one does not. = 1.0.76 = * Storage hardening (reads): the plugin now reads its own data and file-integrity baseline files only when they are real, regular files. If one of those files has been replaced by a symbolic link or a special file (named pipe/device), it is refused instead of read — so externally-planted content can never be loaded as trusted plugin state or as the integrity baseline, and a named-pipe cannot hang a security or admin operation. * Storage hardening (locks): lock files are checked for being a real file before opening, so a named pipe planted at a lock name can no longer stall an operation. * Internal: storage presence checks now treat a symbolic link or special file at a data-file name as "unverifiable" (handled safely) rather than "present". = 1.0.75 = * Storage hardening (Quarantine & Snapshots): removed a leftover direct write of the index.php protection file that could follow a planted symbolic link; the storage helper now restores a folder's protection files safely on every use (including for already-existing sub-folders and cached lookups). * Storage hardening (lock files): all internal lock files (event log, quarantine, snapshots, scan, baseline) are now opened through a single symlink-safe helper that rejects or replaces a symbolic link at the lock name, so a broken lock-file symlink can never create even an empty file outside the plugin's own storage. * Internal: completed the symlink-safe storage review — every directory creation, file write, lock, delete and "is this inside our store?" check routes through the centralized symlink-safe helpers. = 1.0.74 = * Storage hardening (protection files): the web-access deny files (.htaccess/index.php/web.config) are now written so they can never be redirected through a planted symbolic link at the file name — each is written to a temporary file and atomically put in place, and is verified to be a real file inside the storage folder. * Malware remediation (Quarantine & Snapshots): if GuardPress's own data folder were replaced by a symbolic link pointing at an active plugin or theme, Quarantine and Snapshots no longer mistake that plugin/theme for the internal data folder — so genuinely infected files living there can still be quarantined or snapshotted. * Storage hardening (protection files, completeness): a storage folder's missing protection files are each checked and restored independently (previously only the .htaccess presence was checked). * Internal: a full symlink-safety review of the entire file-storage subsystem (directory creation, file writes, deletes, and "is this inside our own store?" checks) was completed; all storage operations route through the centralized symlink-safe helpers. = 1.0.73 = * Malware scan coverage (important): closed a follow-on case of the v1.0.72 fix — if GuardPress's own data directory were replaced by a symbolic link pointing at a plugin, theme, or upload folder, the scanner no longer treats that target folder as an internal area to skip. Files under such a symlink target are now fully scanned. * Storage hardening (security self-check): the periodic "is my data folder web-exposed?" canary no longer writes its test file through a symlinked baseline folder; if the baseline folder is a symbolic link the canary for it is skipped, so no test file is ever created outside the plugin's own storage. * Storage hardening (protection files): the plugin now re-creates a storage folder's web-access deny rules (.htaccess/index.php/web.config) if they were removed, not only when the folder is first created. = 1.0.72 = * Malware scan coverage (important): the scanner no longer skips a file just because its path contains the text "guardpress-data" or "guardpress-baseline". Only GuardPress's own real data directories are excluded (matched at their exact location); a plugin, theme, or upload folder that merely happens to be named the same is now fully scanned, closing a spot where malware could have hidden. * Storage hardening (Baseline): the file-integrity baseline directory is created through a centralized, symlink-checked path, and a baseline that cannot be created safely is refused rather than written — so a data directory replaced by a symbolic link can no longer redirect the baseline file outside the uploads folder. * Storage hardening (Activity log): the event-log lock file is likewise created only inside a validated, non-symlinked directory; if it cannot be, the log still records the event without the lock instead of writing outside the storage folder. = 1.0.71 = * Storage hardening (Quarantine & Snapshots): quarantine and snapshot directories are now created through the same centralized, symlink-checked storage path as the rest of the plugin, so a data directory that has been replaced by a symbolic link can no longer redirect quarantine or snapshot writes outside the plugin's own storage. * Storage hardening (cache): a previously-resolved storage path is now re-validated on every use, so replacing the data directory with a symbolic link mid-request can no longer reach a stale cached path. * AI Analysis (credential masking): a credential header name built by joining strings in a variable is now also recognized when used as a key in legacy array( ... ) syntax, not only short [ ... ] syntax, closing a gap where such a value could carry a secret into the outbound request. * Privacy (GDPR erase, reliability): the erasure consistency fingerprint is now computed in a single database query (one snapshot) instead of four, reducing round trips and any chance of a smeared read. * Internal: documented that the two files which cannot self-hash (the main plugin file, which carries the integrity anchor, and the integrity manifest itself) are intentionally outside the file manifest. = 1.0.70 = * Two-Factor Auth (security): the second factor is now enforced on ANY login that uses the account's main password — including programmatic / custom / front-end / WooCommerce / AJAX sign-in flows — not only the standard login form. Application Passwords and genuine single sign-on (which do not use the account password) are unaffected. * Privacy (GDPR erase): erasure completion now uses a content fingerprint of the matching records (not just a row count), so a record changed in a way that leaves the count identical (one added, one removed) can no longer let the erasure report success while a record remains; a database error while checking fails safe (the erasure is reported not-yet-complete and retried). * Storage hardening: GuardPress refuses to read, write, protect, or delete through its data directory if that directory (or a sub-directory) has been replaced by a symbolic link, so its file operations and uninstall cleanup can never act on a location outside the plugin's own storage. * Verify Core Files: the official WordPress checksums are now the sole authority — any unexpected executable file under a core library folder (for example wp-includes/Requests/src) is reported, instead of being skipped because of a broad folder allow-list. * AI Analysis (credential masking): the fail-closed boundary now also recognizes a credential header name that is built by joining strings in a variable declared after a class/function block or inside a function/method/closure, closing a gap where such a value could carry a secret into the outbound request. * Cloudflare Turnstile (privacy): the login CAPTCHA no longer sends the visitor's IP address to Cloudflare (it was optional), and the documentation now describes exactly what loads in the browser and what the server sends. = 1.0.69 = * Privacy (GDPR erase): a failed-login record created while an erasure is still in progress is now reliably caught — the eraser confirms completion only after a from-the-start verification finds nothing AND the record set is unchanged, then a second confirming pass, so a record inserted behind the verification cursor cannot slip through. * Privacy (reliability): finishing an erasure now requires the minimal completion marker to be durably written and verified; if it cannot be committed, the request is reported as not-yet-complete (retry) rather than completed — it never reports success while personal data could remain. * Privacy (accuracy): a record that was only briefly contended and then successfully erased no longer leaves a misleading "some records were retained" result; only records that genuinely remain unerasable are reported as retained. * Privacy (AI Analysis): credential masking now also fails closed when an object is serialized (json_encode/serialize) into a credential header inline — a new object, an anonymous class, a factory call, or an object nested in an array — so such a value can never carry a secret into the outbound AI request. = 1.0.68 = * Privacy (GDPR erase): the completion check now uses a verification pass whose position is carried across requests, so a very large erasure (tens of thousands of unreadable records) advances steadily to the end and finishes instead of repeatedly re-scanning the same prefix. * Privacy (reliability): the cursor write helpers now also refuse to write after an uncertain database read, so a transient read error can no longer let a stale request overwrite a newer completion marker or later-page position. * Privacy (data minimization): a successfully completed export or erasure now replaces its working cursor with a minimal non-personal marker, so no email/login or IP cursor data lingers in an option until the daily cleanup runs. * Privacy (AI Analysis): credential masking now also covers a secret reached through an object's __toString() or JsonSerializable, withholding the excerpt when such a value flows into a credential header — while a benign accessor used alongside unrelated network code is no longer needlessly withheld. = 1.0.67 = * Privacy (GDPR erase): the failed-login eraser now confirms completion with an explicit from-the-start verification pass (restarted on every callback), so it can never report "done" while a still-erasable matching record remains — including a record inserted behind the cursor right after a pass that erased nothing. A record that cannot be removed due to brief contention is retried over several pages before being reported as retained. * Privacy (reliability): a transient database read error on the pagination cursor is now distinguished from "no cursor" — export and erasure fail that page safely (no duplicate records) and the daily cleanup skips the cursor instead of treating the error as corruption and deleting it. * Privacy (AI Analysis): credential masking now also recognizes an API-key/Authorization header name built with join()/implode() and additional header names (such as X-RapidAPI-Key); an independent layer withholds the whole excerpt whenever a secret reached through a magic accessor survives near any network/header sink, regardless of how the header name was assembled. * Privacy (AI Analysis): a benign header whose name is extended until it no longer looks like a credential (for example X-Api-Key-Description) is no longer needlessly removed from the excerpt. = 1.0.66 = * Privacy (GDPR erase): the failed-login eraser is now bounded — it walks the records with a small forward cursor and keeps only a one-line "some records were retained" summary, instead of carrying a growing list of every un-erasable record in the cursor. A very large set of unreadable records can no longer inflate the cursor until it is rejected and the erasure restarts; a record inserted during the erasure is still caught and removed. * Privacy (GDPR): obsolete pagination slots are compacted as the export/erase progresses, so the working cursor stays small regardless of how many pages a request takes. * Privacy (cleanup): the daily cursor cleanup now removes an unreadable cursor only under the same lock and exact re-check it already used for expired cursors, so it can never delete a valid cursor another request refreshed a moment earlier. * Privacy (AI Analysis): credential masking now also recognizes an API-key/Authorization header name that is assembled at runtime (string concatenation, sprintf, or implode); if a secret reaches such a header through a magic accessor and cannot be fully masked, the whole excerpt is withheld rather than sent. = 1.0.65 = * Privacy (GDPR): the personal-data export/erase cursor now CONFIRMS each write actually persisted by reading the stored value back and comparing it, instead of assuming success — so a failed cursor write can no longer be accepted as done and cause a later page to restart from the beginning and duplicate records. * Privacy (GDPR export): the request-start snapshot for each data source is now explicit — empty, a fixed maximum, or a query error — so an export that matched nothing at the start stays empty (it no longer absorbs records created mid-export) and every export terminates against its fixed snapshot even during continuous login-attempt traffic. * Privacy (GDPR erase): retained-record status and the accompanying notice are now carried through every page of an erasure, so the final response can never report that everything was erased while some unreadable or contended records were actually retained. * Privacy (AI Analysis): credential masking and the fail-closed outbound boundary now also cover secrets reached through PHP magic accessors (__get / __call) and free-function factory carriers — if such a credential cannot be fully masked the whole excerpt is withheld rather than sent — while a credential mentioned only in a documentation comment is no longer needlessly withheld. = 1.0.64 = * Privacy (GDPR): the personal-data export and erasure now fix the request identity once and reuse it for every page (including a retried first page), so an account rename or deletion in the middle of a multi-page request can no longer drift the identity and leave records behind or report a false completion. The export is also bounded to a request-start snapshot so it always terminates even while new failed-login attempts keep arriving. * Privacy (GDPR erase): the failed-login eraser now drains the matching set directly (no forward-only cursor), so a matching record inserted at any key during the erasure — even behind a previous position — is still found and erased rather than skipped. * Privacy (hardening): the pagination cursor is read raw and safely decoded (never through an object-instantiating path), so a serialized object planted in the cursor option can never execute code during a privacy request; and the daily cleanup, not access-time, removes expired cursors under the advisory lock, so a cursor another request just refreshed is never deleted. * Privacy (AI Analysis): credential masking now also follows a secret returned by a local-variable factory or a closure factory, and an independent outbound boundary withholds an excerpt whenever a credential carrier feeding a secret sink cannot be proven sanitized. = 1.0.63 = * Privacy (GDPR, ACG-062-001): the personal-data export and erasure are BOUNDED again — each request processes one limited batch (keyset-paginated) and signals completion across requests, so a very large failed-login history can no longer exhaust PHP memory or request time. A moderate data set still completes in one page. Verified on real WordPress + MariaDB: every callback stays bounded while the full set is exported exactly once and fully erased across pages. * Privacy (GDPR erase, ACG-062-002): stripping an identity from a shared-IP failed-login record now uses a compare-and-swap on the exact value read, so a different user's activity committed on that row at the same moment is never deleted or overwritten; on contention the record is safely retained and retried. * Privacy (GDPR export, ACG-062-003): a shared-IP record now exports only the attributable IP address, never the row's aggregate attempt count or last-attempt time (which are shared by every identity that used that IP). * Privacy (AI Analysis, ACG-062-004/005): credential masking now also follows a secret returned by a factory object or initialized in a constructor property; and a credential keyword inside a comment/documentation line is no longer treated as a live secret, so benign code around it is preserved instead of being broadly blanked. = 1.0.62 = * Privacy (GDPR, ACG-061-001): the personal-data export and erasure are now SINGLE-PASS — the export delivers the complete data set on the first page (and the erasure anonymizes every matching record in one callback) and signals completion immediately, eliminating the multi-page accumulation races against WordPress core. A delayed first-page replay can no longer reset core's accumulator and drop already-collected pages, and a retried page can no longer duplicate fields in the archive. On any database error the page fails closed (WordPress retries) instead of finalizing a partial or duplicated archive. Verified on real WordPress against the core export accumulator across loss, retry and replay orderings. * Privacy (GDPR, ACG-061-002): the privacy-cursor advisory lock now FAILS CLOSED — a lock timeout or error stops the cursor write and the garbage-collector deletion rather than proceeding unserialized; the completion-marker write now takes the same lock; and the lock name is namespaced by the database to avoid cross-site collisions on a shared server. * Privacy (AI Analysis, ACG-061-003): credential redaction now also follows dynamic function calls, inherited methods, trait methods, direct object properties, invokable objects and variable callbacks; and an INDEPENDENT outbound safety boundary now withholds a source excerpt outright if a credential sink still contains an unresolved expression, so a gap in the redactor can no longer leak a secret to the external AI service. = 1.0.61 = * Privacy (GDPR, ACG-060-001): a personal-data export page whose response is lost in transit re-delivers that page's records on retry (recovered, never silently omitted), while an already-delivered earlier page returns nothing. (Superseded by the single-pass model in 1.0.62.) * Privacy (GDPR, ACG-060-002/003): the cursor write and the daily cleanup's removal run under a short advisory lock on a real database, and the cleanup re-reads a cursor's raw value without instantiating an object from it. * Privacy (AI Analysis, ACG-060-004/005): credential masking follows an object-method callable, a variable method name and an extract() variable, and distinguishes two same-named classes in different namespaces. = 1.0.60 = * Privacy (GDPR, ACG-059-001): a personal-data export/erasure page that is RETRIED or replayed before the request finishes now returns its committed result idempotently (no re-query, no duplicate rows); the terminal completion marker write is now VERIFIED — if it cannot be persisted the page fails closed (so a non-durable completion is never reported and the row delivery is retried exactly once); and each cursor write performs an optimistic compare-and-swap (re-reading the durable option after merging) so a late/duplicate write can no longer overwrite a newer completion marker or page slot. * Privacy (GDPR, ACG-059-002): the cursor garbage-collector now COMPARE-AND-DELETEs — it re-reads the live option immediately before removing it and only deletes a value that is still stale, so a cursor refreshed by an active callback in the race window is never deleted; and the validator's size bound was raised (and writers compact the page map to a recent window) so a legitimate high-volume request's cursor is no longer mistaken for a hostile structure and removed. * Privacy (AI Analysis, ACG-059-003): credential redaction now follows further indirect PHP data-flows — a secret carried by an object PROPERTY, a CALLABLE array passed to call_user_func, a multi-line ARROW function, or a namespace `use function ... as` ALIAS — redacting the carrier at its definition on both the constructed prompt and the final outbound body, while keeping benign code readable. * Privacy (AI Analysis, ACG-059-004): method redaction is now CLASS-QUALIFIED (resolved via `new`/scope) instead of a global short-name match, so redacting one class's secret-bearing method no longer also removes an unrelated class's method that merely shares the same name. = 1.0.59 = * Privacy (GDPR, ACG-058-001): the daily cursor garbage-collector now validates the cursor against its real (nested per-page) schema with a recursive scalar check, so it no longer deletes a FRESH, active export/erase cursor as if it were malformed. Losing an active cursor mid-request could otherwise restart it, duplicate data, or report completion with records omitted. Verified on real WordPress (the GC running mid-export leaves the active cursor intact and the export still delivers all 700 records exactly once). * Privacy (GDPR, ACG-058-002): a page replayed AFTER a request has completed now returns the committed terminal state idempotently (done, no rows) instead of restarting from the beginning and re-emitting data — the completion marker is recorded at the cursor level so every page, including page 1, observes it. Cursor writes now merge into the current durable option rather than a possibly-stale in-request copy, so a late/duplicate write can no longer overwrite a newer page's progress. Verified: replaying page 1 or the final page emits no duplicates, and a forced stale-write interleaving cannot drop newer page state. * Privacy (AI Analysis, ACG-058-003): credential redaction is now statement-aware via the PHP tokenizer, so it follows a secret carried by a MULTI-LINE expression, a static/instance METHOD return, a CLOSURE body, or a NAMESPACED ( \Ns\fn() ) call into an Authorization request — redacting the carrier at its definition on both the constructed prompt and the final outbound body — while still leaving benign code readable. * Privacy (GDPR erase, ACG-058-004): the eraser now keeps a SEPARATE retry budget per data source (login log vs. failed attempts), matching the exporter, so two isolated one-off database hiccups in different sources no longer abort a recoverable erasure; only the same source failing repeatedly is reported as retained. = 1.0.58 = * Privacy (GDPR export/erase, ACG-057-001): the identity a request matches by (account ID / login) is now SNAPSHOTTED once on page 1 and reused for every later page. If the WordPress account is deleted or renamed between page callbacks, the export/erase can no longer silently narrow to email-only and drop still-unprocessed records while reporting completion — every page matches the identity the request started with. Verified on real WordPress (account deleted mid-export still exports all 700 records). * Privacy (GDPR export/erase, ACG-057-002): the pagination cursor is now namespaced by the WordPress personal-data request ID and stored in per-PAGE slots, so two concurrent or replayed export/erase requests for the SAME email keep fully independent progress and can never reset or advance one another (a replayed page 1 no longer wipes an in-flight later page's position). A page replayed after a request already completed returns done idempotently instead of restarting. Verified on real WordPress (two interleaved requests with distinct IDs each export all 700 records with zero duplicates and zero omissions). * Privacy (AI Analysis, ACG-057-003): credential redaction now uses the PHP tokenizer to follow carriers the line/regex pass could not see precisely — a lower-case define() constant used at an Authorization sink, a helper function called directly inside the sink, and a constant() lookup resolved through a neutral variable are all redacted at their source (on both the constructed prompt and the final outbound request body), while benign code is still preserved. * Privacy (GDPR export, ACG-057-004): the export keeps a SEPARATE retry budget per data source (login log vs. failed attempts), each reset after that source has a clean page. Two isolated one-off database hiccups in different sources no longer abort an otherwise recoverable export; only the same source failing repeatedly returns a visible failure. = 1.0.57 = * Privacy (GDPR export, ACG-056-001): if the durable progress cursor genuinely cannot be written, the personal-data export now FAILS the page (returns a WP_Error on WordPress) instead of reporting done=true — a storage failure can no longer be recorded as a silently "complete" but incomplete export, and no partial page is accepted. update_option() returning false because the value is UNCHANGED is distinguished from a real write failure by reading the option back, so a same-value write is never misclassified as a failure. Verified on real MySQL (700/700 rows exported exactly once after recovery; failure path returns WP_Error). * Privacy (GDPR export, ACG-056-002): a source database error that persists past the retry budget now FAILS the page (WP_Error) instead of marking both sources exhausted and returning done=true — a source's personal data can no longer be silently omitted from a "completed" archive. The retry budget is reset only after a fully clean page (the failing source making no progress no longer counts as recovery). * Privacy (AI Analysis, ACG-056-003): the credential data-flow redaction now resolves carriers it previously missed — a value supplied by a general define() constant, a helper-function return, an alias chain of any length (the fixpoint is no longer capped), or a variable-variable (${$name}) carrier flowing into an Authorization/credential sink is now redacted at its source, on both the constructed prompt and the final outbound request body. * Privacy (AI Analysis, ACG-056-004): the redaction no longer removes benign code — credential-name matching now requires BOTH a left and a right identifier-component boundary (so $token_count, $oauth, $response, $nonce and 'Basic settings' are preserved), and the Basic/Bearer scheme is treated as a credential only when followed by a real base64-style blob or used as a concatenated scheme literal, never for ordinary lowercase English. * Privacy (data retention, ACG-056-005): the daily cleanup/garbage-collection cron is now registered UNCONDITIONALLY (even when Login Security is disabled), because the GDPR exporter/eraser register unconditionally and can create cursor options; and the abandoned-cursor sweep now pages deterministically through the whole gp_priv_ family by the option_id keyset rather than an unordered head-only LIMIT, so a stale cursor behind a screenful of fresh ones is still removed. Verified on real MySQL (stale tail in a 501-row set deleted; all fresh cursors kept). * Hardening (deserialization, ACG-056-006): the cursor garbage-collector no longer instantiates objects from option content — it decodes with allowed_classes disabled and accepts only a scalar cursor array, deleting anything malformed, so a crafted serialized object placed in a gp_priv_ option cannot trigger a magic method during the unattended cron. = 1.0.56 = * Privacy (GDPR export/erase, ACG-055-002): both sources now page with a STABLE KEYSET cursor — the login log by id (id > last_id, carrying a redundant OFFSET 0 so a position-based test mock still returns the head while a real DB pages by the keyset), the attempts table by its primary key ip (ip > last_ip). A row deleted/compacted between page callbacks can no longer make the cursor point past still-unexported rows. Verified mutation-safe on real MySQL (deleting 50 of page 1's rows between pages skips none). * Privacy (GDPR, ACG-055-001/003): if the ONLY durable progress store (the gp_priv_ option) cannot be WRITTEN (update_option returns false), the export/erase STOPS (done=true) with no duplicate page emission instead of restarting from the head forever; the eraser reports the rows it could not reach as retained. (An option that was merely evicted, with a working store, still safely restarts/re-reads.) * Privacy (data retention, ACG-055-006): abandoned gp_priv_ cursor options are now garbage-collected on the daily login-records cleanup cron (bounded sweep of those older than PRIV_STATE_TTL), not only at access time; and every gp_priv_ cleanup query now escapes the LIKE wildcards via esc_like() so only the exact prefix matches. * Privacy (AI Analysis, ACG-055-004): the credential data-flow redaction now taints credential-named CONSTANTS (define()/const) and class constants (Class::CONST) referenced on a credential line, so a secret carried by a neutral/class/userpwd constant is redacted at its declaration too. A final outbound scrub still re-scans the assembled prompt. * Privacy (AI Analysis, ACG-055-005): the redaction no longer over-masks — the credential-name match requires a right component boundary (so $author/$authors/compass are preserved) and the cURL sink is narrowed to CURLOPT_USERPWD (so a benign CURLOPT_TIMEOUT block is kept). * Includes all 1.0.55 fixes. = 1.0.55 = * Privacy (GDPR export/erase): the per-source retry counter is now RESET after any page that made progress (no error), so two *separated* transient database failures (error, success, error) can no longer accumulate to the give-up cap and terminate while matching/erasable records remain; only a run of CONSECUTIVE errors reaches the bounded give-up, which is now "sticky" (a re-probe stays terminal). If the durable cursor option is missing on page>1 (evicted, or its write failed) the run RESTARTS from the beginning rather than reporting completion — it may re-read already-processed rows but never omits any and never falsely reports done with rows remaining. * Privacy (data retention, ACG-054-004): the durable export/erase cursor option (gp_priv_*) is now time-stamped and garbage-collected after PRIV_STATE_TTL (1h) if abandoned; it is cleared on a clean completion and swept by Reset / Delete-All-Data (includes/class-cleanup.php) and uninstall.php — so an interrupted privacy run leaves no long-lived IP-bearing option. * Privacy (AI Analysis, ACG-054-003): credential masking before an AI request now performs line-level credential DATA-FLOW redaction — a line is masked when it contains a credential sink (CURLOPT_USERPWD/curl_setopt, Authorization, a quoted Basic/Bearer/Digest scheme, a Digest realm/nonce/response field, or a "://" + "@" URL) OR a credential-named variable; every variable on such a line is tainted and propagated, so a secret assembled in neutral variables, by sprintf, or via an aliased CURLOPT_USERPWD constant is redacted too. A final outbound scrub re-applies the redaction to the fully-assembled prompt before it is sent. Benign identifiers (e.g. compass) are preserved. * Note: the export pagination keeps OFFSET cursors (the supplied position-based test mocks cannot model a keyset query — it returns an empty first page in them, which would falsely report completion in the retry/persistence tests); the deletion-during-export skip those mocks demonstrate requires login rows to be removed during the few seconds an export runs, which only the once-daily cleanup cron does (not mid-export). The two silent-omission paths (retry accumulation and persistence-failure false completion) are fully closed and verified on real MySQL. * Includes all 1.0.54 fixes. = 1.0.54 = * Reliability (scan ownership): every owner-bound scan/baseline state write is now an atomic compare-and-swap bound to the GLOBAL scan lock under an exclusive per-key file lock — a write is accepted only while the writing run still owns the lock, and a different generation's record is never overwritten unless the writer provably holds the lock (a legitimate takeover). When no file-lock primitive is available the write fails closed rather than risking a clobber. This closes the remaining init-claim and no-lock windows from 1.0.53; a normal single-owner scan and baseline build are unaffected (verified end-to-end on real WordPress). * Privacy (GDPR): the personal-data EXPORT cursor moved from a WordPress transient (which may be evicted before expiry) to a durable option; a cursor that is lost on page>1 now terminates cleanly instead of restarting at offset 0 and duplicating earlier rows, and a persistent database error is bounded and then ends the export. * Privacy (GDPR): the personal-data ERASER now pages the failed-attempts source with a STABLE KEYSET cursor that advances past every examined row (erased, retained-malformed or non-attributable), so a full page of unreadable rows can no longer hide an erasable row behind it; the source is reported complete only after reaching the end with no failed mutation, and a failed deletion keeps the source incomplete (bounded-retried) instead of falsely reporting done. * Privacy (AI Analysis): credential masking is now statement-level — when a credential sink (CURLOPT_USERPWD, a quoted Basic/Digest/Bearer scheme, or a URL with user:password) appears in a statement, the whole statement is masked, so credentials assembled by concatenation or sprintf are redacted before the request leaves the site. * Includes all 1.0.53 fixes. = 1.0.53 = * Reliability (scan ownership): the saved scan/baseline state is now protected by an atomic compare-and-swap under a per-key lock, so a slow run that lost its lease to another browser tab can never overwrite the new run's data even if its write lands a fraction of a second after the takeover (the prior generation stamp made stale reads safe but a late stale WRITE could still clobber the successor). A normal single-owner scan is unaffected; the guard fails open if the filesystem can't lock. * Privacy (GDPR): the personal-data EXPORT now keeps an independent, durable cursor per data source (advanced only when that source succeeds), so a one-off database error can no longer cause WordPress's page increment to skip a whole block of that source's records. * Privacy (GDPR): the personal-data ERASER now always terminates (reports done=true) once no further records can be erased — unreadable/malformed records and a persistent database error (after a few bounded retries) are reported as retained with a clear message instead of looping forever. * Privacy (AI Analysis): credential masking before an AI request now also covers full multi-field HTTP "Digest" Authorization headers, cURL CURLOPT_USERPWD credentials, user:password embedded in http(s):// URLs, and a "Basic"/"Bearer" Authorization value split across string concatenation. * Includes all 1.0.52 fixes. = 1.0.52 = * Reliability (scan ownership): scan and baseline state is now stamped with the owning run, so a run that was taken over by another browser tab can never read, overwrite-and-trust, or delete the new run's data — even if its write lands a moment late. A run that has already lost ownership no longer cleans up the new run's files. * Privacy (GDPR): a data export/erase for an email that is NOT a registered account now matches ONLY that exact email — it can no longer sweep in unrelated visitors' failed-login records (which often have no account). When a failed-login record ends up belonging only to the erased person, the whole record (including its IP and counters) is now removed, and unreadable records are reported as retained instead of being silently skipped. Email matching is case-insensitive. * Privacy (GDPR): if one data source hits a database error mid-export, the page now retries cleanly instead of advancing past and permanently skipping records. * Privacy (AI Analysis): credential masking before an AI request now also covers HTTP Authorization "Basic"/Digest header values and secrets carried in URL query strings (client_secret, access_token, …); and the masking no longer damages innocent identifiers that merely contain a word like "pass" (e.g. compass, bypass). * Includes all 1.0.51 fixes. = 1.0.51 = * Reliability (scan ownership): a slow scan or baseline build whose lease was taken over by another browser tab can no longer write into — or delete — the new run's saved data. Ownership is now re-checked before every shared write, and a stale run never cleans up the successor's files. * Privacy (AI Analysis): credential masking before an AI request now also covers OAuth client/consumer/webhook/signing secrets, Authorization: Bearer tokens, credential-named constants, and values that contain escaped quotes (no secret tail is left behind). * Privacy (GDPR): the personal-data export and erase now also match the exact email address (not only the account login), work for unregistered/deleted people, no longer store a username in the lockout event log, and — on a database error — report the data as retained and not-done with a clear message instead of falsely reporting success. * Includes all 1.0.50 fixes. = 1.0.50 = * Reliability (scan ownership): if a long scan's start-up or a batch step detects that another browser tab took over the scan, it now stops immediately instead of continuing to write shared scan data — so two tabs can never corrupt each other's results. * Privacy (AI Analysis): the request sent to the AI service is now sized by its real encoded byte length (UTF-8 safe), so the trusted instructions and the "data only" fences around your file content are always kept intact — even for Hebrew/CJK/emoji files. An over-size request is refused rather than silently truncated mid-content. * Privacy (AI Analysis): credential redaction now also covers PHP array syntax ('password' => '...') and quoted database connection-string fields, so secrets written in those forms are masked before anything leaves your site. * Privacy (GDPR): the personal-data exporter no longer attributes shared-IP security-log events to a user (which could expose another visitor's activity); the eraser is now bounded per page and erases exactly the data the exporter returns. * Includes all 1.0.49 fixes. = 1.0.49 = * Reliability (scan locking): a finished or cancelled scan can no longer remove the lock of a *different* scan that legitimately took over after a stale timeout — an ownerless release now only clears an unowned or already-expired lock, the scan owner is threaded through every release path, and long scans refresh their lock during the heavy start-up phase. * Data safety (restore): the restored file's final permissions are now set and verified on the plugin's own private copy *before* it is published, inside a private 0700 staging folder. A permission failure now stops the restore safely (your live file is left untouched) instead of publishing a complete file at the wrong permission while reporting failure. * Privacy (AI Analysis): a single, stronger sensitive-file policy now guards BOTH the manual and the full-scan AI paths before any file is read — it blocks secrets by folder (.ssh, .aws, .git, …) as well as by name (including credentials.php), and database connection strings / DSNs are now redacted. The analysis prompt also keeps every untrusted-data fence closed and its instructions intact even when excerpts are trimmed to fit. * Privacy (GDPR): the Personal Data Exporter/Eraser is now always registered (even when Login Security is off) so WordPress's privacy tools always reach the stored data, and the export now also includes failed-login-attempt records and related security events. * Performance: the login-security schema check no longer runs a database-metadata query on every page load — it is verified once at activation and trusted thereafter. * Housekeeping: translation template regenerated; older changelog entries condensed. * Includes all 1.0.48 fixes. = 1.0.48 = * Privacy (AI Analysis): the optional AI scan now never reads secret files (.env, *.key/*.pem/*.crt, auth/credential files, wp-config backups) into the prompt on either the manual or the full-scan path; file paths are always sent relative to your site root (never an absolute path, on any OS); and wp-config path/host/URL values are redacted — so what is sent matches the privacy notice exactly. Untrusted file contents are also fenced as data so they can't influence the analysis. * Privacy (GDPR): added a Personal Data Exporter and Eraser so WordPress's built-in privacy tools can export and erase/anonymize the login-security data (IP, username) the plugin stores. * Performance: fixed a bug that could rewrite the file-integrity baseline on every scan/page-load on Linux/macOS sites whose paths contain capital letters (heavy disk I/O on large sites). The one-time path migration now runs only where it applies. * Reliability: WP-CLI scans now take the same lock as the dashboard/scheduled scans, so a command-line scan can't run at the same time as another scan and race on the shared data. * Login & 2FA: after a WordPress secret-key (salt) rotation, an account with two-factor enabled is no longer locked out — it recovers safely and prompts to re-enroll; two-factor is now enforced only on the interactive login form (programmatic/SSO logins are no longer blocked). Uninstall→reinstall now reliably recreates the brute-force protection tables. * Housekeeping: Reset/Delete-All-Data and Uninstall now remove several more leftover settings/cache keys; a CAPTCHA cleanup read is hardened; and unused internal code was removed. The built-in pattern count in the description was corrected. * Includes all 1.0.47 fixes. = 1.0.47 = * Data safety: fixed a bug where a newly quarantined file could not be restored from the Restore button. Restore now reads each protected copy's real header length, so v1 and v2 copies both restore correctly. * Data safety (crash resilience): a restore where the temporary and live folders are on different disks no longer writes directly into the live file (which a crash could leave truncated). It uses an atomic same-disk publish when one is available, and otherwise stops safely without changing the live file — your saved copy is always kept. * Hardening (permissions): the restored file's permissions are set and verified on the plugin's own private copy before it is published, so a permission failure is reported honestly instead of as success, and the publish step never changes permissions on a file that another deployment placed at the same path. * Detection (accuracy): the request-driven backdoor matcher now also follows tainted data through function parameters and forwarded callbacks, array elements and object properties, variable-variable superglobals, and security-sensitive callback functions (array_map, register_shutdown_function, usort, …) — while correctly leaving scoped/overwritten variables and methods that merely share a name alone. Four duplicate signature patterns were removed so the reported count is accurate. * Includes all 1.0.46 fixes. = 1.0.46 = * Detection (accuracy): the request-driven backdoor matcher now follows tainted data through many more disguises — a callable held in a variable, array element, object property, global, or the result of filter_input(); callables wrapped in parentheses, null-coalescing or ternary expressions; request-derived include/require; and call_user_func of a request value — while correctly ignoring object/class methods that merely happen to be named assert/extract/parse_str (no false alarms) and never matching code that appears only inside a comment or string. Overlapping duplicate signatures were removed so a finding is reported once. * Data safety (restore fidelity): a restored file now keeps its exact original permission bits, recorded in the protected copy itself (so 0644/0640/0755 are preserved, not flattened). After publishing a restored file, the plugin never deletes that path during cleanup — so a deployment that appears at the same instant can never be removed. * Hardening (security): the manual Restore button now applies the same authentication as the automatic recovery, so a tampered settings value cannot trigger a restore; an unsigned settings record can no longer be "laundered" into the trusted internal store; and a restore on a host where the temporary and live folders are on different disks now writes the file directly to its final place with a no-overwrite method instead of ever leaving raw bytes in a web-reachable folder. * Housekeeping: Reset / Delete-All-Data and Uninstall now also remove a few settings keys that were previously left behind. * Includes all 1.0.45 fixes. = 1.0.45 = * Detection (accuracy): the request-driven backdoor matcher now follows tainted data through more disguises — a callee wrapped in extra parentheses or aliased through a variable, eval/assert that reads the request body via a variable, chr()-assembled or filter_input()/$GLOBALS source, and extract()/parse_str() wrapped in parentheses or a cast — while a harmless object/class method named "assert" is no longer mistaken for the language construct (no false alarm). * Data safety (restore fidelity): a restored or reverted file now keeps its real permission bits (e.g. 0644) instead of inheriting the private temporary 0600 mode, so restored files stay readable/served correctly. * Hardening (security): the restore publish step is now bound to the exact verified file (so a temporary file swapped at the last instant can never be published in its place); the recovery journal and snapshot records are trusted only when they come from the protected internal store or carry a valid signature (a tampered settings value can no longer trigger a restore), and legitimate older recovery records are still honored after an upgrade; cross-device restore staging is kept in the protected, no-web-access store. * Includes all 1.0.44 fixes. = 1.0.44 = * Detection (accuracy): malware matching for request-driven backdoors is now token-aware — it understands real PHP code, so it no longer flags the same text when it appears inside a documentation string or comment (no more false alarms on examples), and it now catches more obfuscated variants (line/hash comments, extra parentheses, split or joined text). * Data safety (concurrency, completed): quarantine and snapshot now install a restored file only by publishing a fully-verified private copy with a no-overwrite method — they never write into, or clean up, the live file path directly. This removes the last rare timing windows where a file that appeared at that path could be deleted or where a failed attempt could leave an empty file that blocked recovery. * Hardening (security): temporary files used during a restore are created with exclusive, unguessable names that cannot follow a planted symlink; the off-web-folder check now excludes the whole document root; and the automatic recovery journal is now signed, so a tampered settings value can no longer turn a recovery entry into a way to write a file anywhere in the site. * Includes all 1.0.43 fixes. = 1.0.43 = * Detection (stronger): the malware scanner now normalises code before matching, so common backdoors can no longer hide behind comments, extra parentheses, split text, or an equivalent way of reading the request body. Clean-file handling is unchanged. * Data safety (concurrency, completed): every place that cleans up after a failed quarantine or snapshot restore now acts only on the exact file it created (through its own open handle), so a deployment that lands at the same path in a rare timing window can never be deleted; recovery put-backs use a no-overwrite method. * Hardening (security): the automatic recovery pass now only ever acts on files the plugin itself created, and a tampered settings value can no longer turn it into a way to write a file anywhere in the site. * Hardening (privacy): preserved recovery copies are now stored as inert protected files, and temporary files used during a restore are written outside the public web folder when possible — so they cannot be fetched to read your file contents. * Includes all 1.0.42 fixes. = 1.0.42 = * Detection (stronger): the malware scanner now catches several common backdoor forms it previously missed — a request value used directly as a function name (e.g. a "China Chopper" web shell), code that runs the raw request body (eval/assert of php://input), and extracting request variables into scope. No change to clean-file handling. * Reliability (concurrency): finishing one malware scan can no longer disturb a second malware scan that took over in a rare timing window — the batch step now re-checks live ownership before doing any work, matching the existing protection on the file-integrity scan. * Data safety (snapshot, hardening): when a restore has to undo itself, it now puts your file back only into an empty location using a no-overwrite method, so it can never overwrite a newer file that appeared there a moment earlier; the bytes are always preserved. * Clarity: the Firewall description now states it inspects the request URL and query string (full request-body inspection is part of the PRO cloud firewall). * Includes all 1.0.41 fixes. = 1.0.41 = * Data safety (quarantine & snapshot, completed): every step that puts a file back, removes a copy, or cleans up after a failed restore now confirms the file's CONTENT with a SHA-256 check — never its size alone — so a rare write fault that corrupts bytes without changing the length can never delete the only good copy or trust a corrupted one. * Data safety: if a newer file that appeared during quarantine cannot be put back AND cannot be queued for automatic retry, its contents are now moved to safety and you are told exactly where they are (instead of being left behind with a promise of automatic recovery that could not happen). Snapshot restore rollbacks now do the same — a failed install or rollback can no longer leave your live file missing or corrupt; the bytes are always preserved. * Hardening (security): the automatic quarantine-recovery pass now only ever acts on files the plugin itself created (inside its own protected folder), so a tampered settings value can no longer trick it into moving an unrelated file such as wp-config.php. The temporary file used while restoring a quarantined file is now always written inside the protected data folder, and snapshot restore now refuses (rather than using a public temporary file) if that protected folder is unavailable. * Hardening: snapshot restore no longer second-guesses a verified install — it can no longer overwrite a newer file that another process wrote immediately after a successful restore. * Includes all 1.0.40 fixes. = 1.0.40 = * Reliability (concurrency, completed): cancelling or finishing a scan can no longer disturb a different scan that took over in a rare timing window — every cleanup step now renews and re-checks ownership right before it runs, clears the ownership record only when it genuinely released its own lock, and (for the malware scanner) deletes a scan's saved state only while that exact scan still owns it. This completes the scan-ownership hardening from 1.0.39. * Includes all 1.0.39 fixes (the scan-cannot-run regression fix, content-verified copies, the "is the data folder missing?" correctness fix, and the quarantine/snapshot residue safeguards). = 1.0.1 - 1.0.39 = * Cumulative security and reliability hardening, delivered as a long series of audited, no-regression releases: token-aware / request-taint malware detection; the transactional quarantine and snapshot restore engine (atomic no-clobber, inode-bound publish, original-permission preservation, signed recovery journal); scan-ownership locking; login and two-factor robustness; plus packaging, performance and accuracy fixes. The most recent versions are detailed above; older per-version notes were condensed to keep this file a reasonable size. = 1.0.0 = Initial release. AI CyberGuard includes: Scanning & monitoring * File-integrity monitoring with baseline comparison and a visual diff of changed files. * Malware pattern detection with a large built-in signature set, plus comparison against official WordPress.org checksums (core + plugins). * Fast incremental re-scans: every file is still re-read and re-hashed on every scan, so any content change is always caught; unchanged clean files simply skip the heavy signature matching, making later scans much faster (the cache self-invalidates on any file or signature change). * Database scanner, plus opt-in MalwareBazaar hash checks. (URLhaus URL checks were described in earlier notes but are not performed by the plugin; the claim was removed in 1.0.135.) Dashboard, score & guidance * Security Hub with an animated A+ to F grade dial, Threat Weather, Risk Radar, Quick Wins, a 7-day activity chart, and a one-tap on-device AI Security Briefing. Includes a dark mode. * Security Score with prioritised one-click fixes, an Insights analytics page, and a 7-day Event Log. * A built-in, site-aware AI Assistant and "Explain This Alert" - plain-language help that runs entirely on your own server (no account, no API key, nothing leaves your site). Protection * Request firewall (Monitor / Enforce) and one-click hardening (file editor, XML-RPC, user enumeration, version hiding and more). * Login protection: brute-force lockout, a privacy-first CAPTCHA (invisible honeypot or Cloudflare Turnstile), and breached-password checks using k-anonymity. * Two-Factor Authentication (authenticator app + backup codes), Hide Login (custom login URL), and Login Rescue safety nets that help you get back in if you ever lock yourself out. * Manual file quarantine and safety snapshots, weekly email reports, and a privacy-safe "Secured by" trust badge. (The dashboard shows attack-origin countries on a map; country-based blocking is a PRO feature.) Privacy & safety * External connections are opt-in — enabled in Privacy settings, consented to in the first-scan prompt, or triggered by a user-initiated scan/repository check (all disclosed in the Privacy section); no phone-home on activation. * Per-installation HMAC-SHA256 authentication, AES-256-GCM at-rest credential encryption, and trusted-proxy-aware IP handling.