=== Guard Forge – Security, Login Protection, 2FA & Audit Log ===
Contributors: avakodeforge
Tags: security, login-protection, two-factor-authentication, audit-log, brute-force
Requires at least: 6.5
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 1.9.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html

WordPress security plugin: brute-force login protection, two-factor login, file-integrity checks and a tamper-evident audit log. No account needed.

== Description ==

**Guard Forge** hardens your WordPress site against common attacks without an account or an API key. It applies hardening rules, watches for brute-force login attempts, protects accounts with two-factor authentication, compares your files with what wordpress.org actually published, and keeps an audit log that shows if anyone edited or deleted a line afterwards.


**Login protection.** Failed logins are counted per IP address; past the threshold the address is locked out and unblocked automatically later. Every logged-in user can switch on two-factor authentication for their own account: a time-based one-time code (TOTP), a login challenge and single-use recovery codes. The QR code for enrolment is drawn on your own server, so the secret never leaves the site.

**File integrity.** Once you switch the monitor on, every core file and every plugin hosted on wordpress.org is compared with the checksums wordpress.org publishes for that exact release. The screen separates files that are official, modified, missing, and not in the official build at all, which is what a backdoor looks like. Premium and custom plugins and all themes have no published checksums, so they are marked "no reference" and never counted as clean. The check asks wordpress.org with a version number and a plugin slug, and never sends your site address or anything about your content.

**Audit log.** Logins, failed attempts, option changes, plugin and theme activity and user and role changes are recorded, and every row carries the hash of the row before it. "Verify chain" walks the table: an edited row no longer matches its own hash, and a deleted row leaves the next one pointing at a hash nothing produces.

**Hardening score.** Twenty-one checks of your own installation add up to a score from 0 to 100. Each row states how many points it is worth and links to the screen that changes it. The score makes no outbound request.

= Free =
* Hardening score: twenty-one checks, 0-100
* Login brute-force protection with per-IP lockout and automatic unblock
* Two-factor authentication (TOTP) with recovery codes for every logged-in user's own account
* File integrity against the official wordpress.org checksums (off until you switch it on)
* Hash-chained audit log with chain verification
* Hardening: disable XML-RPC, remove version headers, protect wp-config.php through .htaccess
* Content-Security-Policy header, report-only by default
* IP management: allowlist, lift a lockout, login-log browser
* Spam-bot honeypot on the comment form
* Core file protection rules (.htaccess)
* Staging mode: on a copy of your site lockouts are recorded but not enforced, and no notification, alert or firewall rule leaves the copy

= Pro add-on (separate plugin, not part of this download) =
* Two-factor enforcement for administrators
* Alerts to Telegram, Slack and a signed webhook
* Lockdown mode
* Real-time vulnerability database
* Patch by update (off until you switch it on)
* Geo-IP blocking and Cloudflare firewall sync
* Managed firewall ruleset (SQL injection, local file inclusion, XSS, PHP injection)
* Malware scanner
* AI threat analytics
* Audit log export to CSV

The only request this plugin makes is the checksum lookup to wordpress.org, and only while the file-integrity monitor is switched on. See External services below.

Documentation: https://guardforge.app/docs/

== External services ==

This plugin reaches wordpress.org, and nothing else. Activating it contacts nobody and schedules nothing that would — the file-integrity monitor is off on a fresh install, and switching it on is what puts the daily lookup on the schedule. The only outgoing request it ever makes is the checksum lookup below, it happens only while the file-integrity monitor is switched on, and it asks a public catalogue a question that names nothing of yours.


**wordpress.org, for the file-integrity scan.** Off until you switch the monitor on in GuardForge > Settings. Then once a day, and whenever you press "Run scan now", GuardForge asks api.wordpress.org for the checksum list of your WordPress version and locale, and downloads.wordpress.org for the checksum list of each installed plugin it hosts (its folder name and version number). Those are public catalogues: the request carries the version, the locale and the slug, and nothing else — not your site's address, not your user list, not your content, not a licence key. Answers are cached, and one scan makes at most ten requests, so a site with sixty plugins is covered over a few days rather than in one burst. Switch the file-integrity monitor off in Settings and none of this happens.

* Endpoints: https://api.wordpress.org/core/checksums/1.0/ · https://downloads.wordpress.org/plugin-checksums/
* WordPress.org privacy policy: https://wordpress.org/about/privacy/ (wordpress.org publishes no separate terms page for this lookup)


Nothing about your site is sent on a schedule or in the background. A fresh install makes no request at all and schedules none; with the file-integrity monitor switched on it has one daily request — the checksum lookup, while that monitor is on — and it asks a public catalogue a question that names nothing of yours. Switch the file-integrity monitor off and this plugin makes no outgoing request at all.


== Installation ==

1. Upload the `guardforge` folder to `/wp-content/plugins/`, or install through Plugins → Add New.
2. Activate GuardForge through the Plugins menu.
3. Open GuardForge → Settings to review the hardening options.
4. Visit GuardForge → File integrity to create the initial baseline, and GuardForge → 2FA to protect your own login. Point your other users at GuardForge → 2FA as well — it is the one GuardForge screen every logged-in role can open, and it only ever touches the account of whoever is looking at it.

== Uninstalling ==

Deleting GuardForge always removes one thing: your two-factor enrolments. That table holds TOTP shared secrets and single-use recovery codes — credential material — and once the plugin is gone nothing can use them, while a database that outlives the plugin gets backed up, exported and copied to staging. Reinstalling means enrolling again, which takes half a minute; leaving shared secrets behind has no upside at all.

Everything else stays by default: your login log, file-integrity snapshots, audit trail and settings all survive a delete, so removing the plugin by accident does not take your history with it. If you want a clean slate, tick **Delete GuardForge data on uninstall** in GuardForge → Settings before you delete.

When the last Forge plugin or add-on on a site is deleted, everything Forge stored on the site is removed, on every site of a network: the statistics settings and the install ID, the shared job queue with its scheduled events, and the other settings and caches the Forge plugins share. While another Forge product stays, only this plugin's own share goes. The paragraphs above are about this plugin's own content.

== The hardening score ==

GuardForge → Hardening score works out one number from twenty-one checks of this installation. Every check reads your own site — options, constants, your .htaccess, your user list, WordPress's own update transients. Nothing is fetched from anywhere, which is also why the score cannot verify a header by fetching your homepage: the score makes no external request of any kind, and that promise is worth more than the extra check.
The one thing in this plugin that reaches the network — the integrity scan's checksum lookup — is described above, and it is not the score: the score reads what the last scan already worked out and asks nobody anything.

A check that cannot apply here leaves the sum entirely rather than scoring zero. An nginx site has no .htaccess to write, so the two .htaccess checks go to "does not apply" and the remaining checks grow to fill the hundred. Marking a correctly configured site down for lacking an Apache file would be theatre.

Every check sits in one of four weight buckets, and there are no others. 12 is the control whose absence is how sites actually get taken over; 8 is a direct route in, or the loss of the evidence that one was used; 5 is reconnaissance and exposure that shortens somebody else's work; 2 is worth doing, cheap, and not what the incident report will name. The full table:

* `https` — 12
* `two_factor_admins` — 12
* `updates_pending` — 12
* `debug_display` — 8
* `xmlrpc` — 8
* `brute_force` — 8
* `file_edit` — 8
* `integrity_baseline` — 8
* `admin_username` — 5
* `user_enumeration` — 5
* `rest_restricted` — 5
* `security_headers` — 5
* `csp` — 5
* `sensitive_files` — 5
* `audit_log` — 5
* `hide_version` — 2
* `directory_listing` — 2
* `login_captcha` — 2
* `pingback` — 2
* `bad_useragents` — 2
* `notifications` — 2

The points a row advertises are the points you actually gain: the weights above are shared out over the checks that apply to your site so that they add up to exactly one hundred, and the headline is the sum of what those rows earned. With the Pro add-on installed and licensed, one more row joins the same list — installed plugins and themes with a published advisory, at 12 — and the shares are worked out again around it. Without the add-on that row does not exist and nothing on the screen mentions it.

= The public badge =

Off unless you switch it on. When you do, GuardForge issues a link on your own site that publishes a letter grade and the month it was worked out — nothing else.

The grade is not your score. It is worked out only from the eleven checks a stranger can already run against your site from outside with no login: the scheme, XML-RPC, whether `?author=1` gives up a login name, whether the REST API hands your user records to anonymous callers, the version in your generator tag, your response headers, your Content-Security-Policy header, whether a directory lists its contents, whether sensitive files answer directly, the login form, and the X-Pingback header. Everything on that list is already public, so the badge tells a passer-by nothing they could not have found by loading your site.

Your numeric score, your two-factor coverage, your brute-force thresholds, your integrity results, whether an account is called admin, your pending updates and any vulnerable components are never published, in any form. The link carries an unguessable token so nobody can walk a list of sites looking for weak ones, it can be reissued or turned off at any moment, views are never logged, and until you switch it on the address is not registered at all — your site answers WordPress's own 404, exactly like a site without the plugin.

== Frequently Asked Questions ==

= Do I need an account or API key for the free version? =

No. Every protection runs on your own server. The one thing that reaches the network needs no account: the file-integrity scan asks wordpress.org for the official checksums of your WordPress version and your wordpress.org plugins, and says nothing about you. See External services above.



= How would I know if somebody edited the audit log? =

Press "Verify chain" on the audit-log screen. Every row carries the hash of the row before it, so an edited field breaks its own row, and a deleted row leaves the next one pointing at a hash the table cannot produce. The check names the first row that failed. Restoring a database backup over a live log breaks the chain too. On a log larger than fifty thousand rows the check covers the oldest part and says plainly that it was truncated.

= What does "not in the official build" mean? =

The file-integrity screen sorts files into official, modified, missing, and not in the official build at all. The last group is a file that the release published by wordpress.org does not contain. A backdoor looks like that, and a comparison with yesterday's scan cannot see it, because a shell that was already on disk when the baseline was taken looks exactly like an unchanged file.

= Will it update my plugins behind my back? =

No. The free plugin does not update anything. The Pro add-on has "Patch by update", which is off until you switch it on. When an advisory names the version that fixes it and WordPress is offering an update that reaches it, the add-on installs that update, one plugin per run, with an allow list and a deny list, and tells you by e-mail and in the audit log.

= Does Guard Forge send my data anywhere? =

Nothing about your site reaches Avakode from this plugin. Its one outgoing request is the checksum lookup to wordpress.org, and only while the file-integrity monitor is on. The Pro add-on is a separate plugin; its own readme lists what it sends and where.

= Will Guard Forge conflict with other security plugins? =

Guard Forge does not detect or switch off other security plugins. If two plugins both count failed logins or both write rules to `.htaccess`, they can overlap, so keep each of those jobs with one plugin. If you notice a conflict, please open a support thread.

= How do I get the Pro features? =

Pro features ship in a separate Guard Forge Pro add-on. Install this free plugin first, then add Pro from https://avakode.com.

== Screenshots ==

1. Hardening score: checks of your own installation scored 0-100, recorded once a day so you can see the trend.
2. The checklist behind the score: each row says what it found, how many points it is worth, and links to the setting that changes it.
3. File integrity against the checksums wordpress.org publishes: here a PHP file that is not part of the official WordPress build, found in wp-includes.
4. Login log: failed attempts per address, and the lockout once the threshold is reached.
5. Hash-chained audit log: user, role, option and plugin changes, and "Verify chain" confirming that every row still points at the one before it.
6. Two-factor authentication for your own account: the enrolment QR code is drawn on your own server and nothing is fetched from anywhere.

== Changelog ==

= 1.9.1 =
* Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.

= 1.9.0 =
* Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.

= 1.8.1 =
* Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.
* Background jobs: on a site where WP-Cron is switched off (DISABLE_WP_CRON), queuing a job no longer sends the request that wakes WP-Cron. The site's own cron runner starts the job, as you set it up.

= 1.8.0 =
* Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.
* Removed unused Freemius integration code.

= 1.7.0 =
* Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.
* Audit log: moving the site to another address is no longer reported as tampering. New rows are checked against every address Guard has known the site by (a new domain, http to https, a move into or out of a subfolder, a copy from a test site to the live site, an address changed in Settings without a search-and-replace in the database), including rows that visitors wrote before an administrator first signed in. A row changed in any other way is still reported as "Chain broken".
* Rows written before this update still do not match after a move. "Verify chain" now says that a move may be the reason and lets an administrator accept the new start of the chain. Only rows that a move explains are accepted, and only as they are now: a later change to one of them is reported again. Your name is written to the log, and nothing is accepted while the chain is also broken for another reason. Accepting needs the sealed mark that this update sets (where the log began to write rows the new way): if it was removed, or the site's security keys (salts) were changed after the update, the check shows the break, says why, and does not offer to accept.
* A row that the check cannot read to the end (it names more than ten different addresses of the site, or the check has used up its budget of hashes) is shown as "could not be checked": not intact, not broken, and it cannot be accepted. The screen says what such a row is and when the retention clean-up removes it; until then the log stays yellow. All other rows are still checked.
* If the update of the database cannot add its new columns (usually the database user has no ALTER permission), a notice in the admin names the columns, says what each one costs, and gives the statement to run. It goes away by itself once the columns exist.
* "Unblock" in the list of lockouts also lifts a block that the web server counted under the other spelling of an IPv4 address (a.b.c.d and ::ffff:a.b.c.d), and the audit log records each lift.
* With the Pro add-on's tools for AI agents, the audit log no longer adds a row each time the tools rewrite their own call journal. A change to the tools' settings is still logged.
* The form plugin is called Fillin Forge, not Form Forge, in the list of REST routes that stay open and in the label of Forge e-mails.
* The new and changed texts are translated into the fifteen languages the plugin ships.
* Deleting the last Forge plugin or add-on now also removes the settings, journals and rate counters of the AI-agent tools.
* Internal, no change in behaviour: the shared job queue gives back the number of the job it stored and asks WP-Cron to run once, at the end of the request.

= 1.6.5 =
* Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.
* Vulnerability ranges such as "<=1.2.3" or "<2.0.0" are matched again. They used to be saved with the "<" turned into "&lt;" and never matched, so a site running a vulnerable version looked clean. Ranges are now checked against a strict pattern, and the ones already saved on your site are read correctly without any action from you.
* GuardForge no longer intercepts the second step of the official Two-Factor plugin. Its code page answers only to its own action, so administrators who use both are no longer sent back to the login form. A page that was open before the update still works.
* A successful two-factor code now counts as a login: the login log shows it and the address's failed-attempt counter is cleared. An accepted authenticator code can no longer be used a second time within its validity window, and a wrong code now says so on the code page instead of showing the form again with no explanation.
* "Verify chain" no longer reports a broken chain on the first row left after the nightly clean-up of old rows. The clean-up keeps a signed starting point. If that starting point no longer matches the site's security keys (salts), because the keys were changed since the last clean-up or because the record itself was altered, it cannot be checked: the screen says so in its own words instead of "Chain broken", checks every later row as usual, tells you not to accept the new start if you did not change the keys yourself, and lets an administrator accept it. Who accepted it is written to the log. Changing a row after that point still reports "Chain broken".
* The audit log now records an address being blocked or unblocked, and one row for each plugin or theme an update touched. The licence refresh stamp that add-ons rewrite on their own is no longer logged as an action of user 0.
* The login log records attempts even when brute-force protection is switched off. Repeated application-password requests from an address that is already locked out are no longer counted again.
* The login captcha asks for an answer when the box is left empty and keeps the question, instead of treating it as a wrong answer.
* The "Disable XML-RPC" switch now says what stops working (the Jetpack connection and the WordPress mobile app), and the Guard screen warns when Jetpack, Jetpack Boost or Jetpack Protect is active while XML-RPC is off. The block matches the request path (xmlrpc.php), not words in the query string, and also refuses a request that WordPress itself marks as XML-RPC, whatever its address.
* "Hide WordPress version" now also covers the style and script links on the login page.
* The file-integrity card says so when no list of official checksums was received, instead of reading as an all-clear.
* The Cloudflare page and the first screen show when Cloudflare refused to block an address, so a token that can read the zone but not write rules is no longer shown as healthy.
* A refused administrator action now answers with an access-denied page (403) in every GuardForge screen and action, not a server error (500).
* The logo in the GuardForge admin header is shown again; it had been replaced by a broken image on every GuardForge screen.
* Two-factor sign-in checks the password before the code: a wrong password no longer uses up a code, and the error for a wrong password no longer depends on the code typed.
* Turning on two-factor tells you when the database refused to store it, instead of showing recovery codes for a setup that was not saved. On a database that cannot add the new column, two-factor is still saved; the check against reusing a code starts once the column exists.
* The database upgrade records the new schema version only when the new two-factor column is really there. If the database user may not alter tables, the upgrade is tried again every hour instead of being marked as done.

= 1.6.4 =
* Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.
* The REST API is no longer closed to visitors by default. Until now GuardForge refused every anonymous REST call outside a short list, which stopped the block-based WooCommerce cart and checkout, Contact Form 7, webhooks and other public REST routes for your visitors. The default protection now keeps only user records private (the REST user list and single user records, and the author name and archive link that an embed answer and the users sitemap give out), for visitors and also for customers and subscribers. Sites that already had "Restrict REST API" switched on with the list of exceptions left as it was are moved to this protection by the update. A site that had written its own list of exceptions keeps the whole REST API closed to visitors, with that list: the new separate setting is switched on for it. Either way a notice in the admin, shown once and closable, says what was chosen and where to change it. Refusing every anonymous REST call is now a separate setting, off by default for everybody else; the Settings screen says what it stops (cart, checkout, forms, webhooks) and which active plugins would be affected.
* The REST address is read the same way with pretty permalinks (/wp-json/) and with plain ones (?rest_route=), so an allowed route is no longer refused on a site with plain permalinks.
* A wrong two-factor code now counts towards the same lockout as a wrong password, so a stolen password cannot be paired with guessed codes without limit. A locked-out address is turned away at the code step too.
* The login lockout now holds on every login form: WooCommerce My Account and other forms that use the standard login, and on REST requests with an application password. Failed application-password attempts count as failed logins (one per request), and a locked-out address is refused even with the right password. An administrator who is signed in by cookie behind a server password prompt (htpasswd) is not mistaken for a guess: the block editor keeps working from a locked-out address.
* The login captcha is now checked only where it is shown. Logging in through WooCommerce My Account or other forms without the captcha field no longer fails every time.
* "Unblock" in the list of lockouts now lifts the block for the address; it used to leave it in place.

= 1.6.3 =
* Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.
* The plugin is now named "Guard Forge – Security, Login Protection, 2FA & Audit Log", translated in the 15 languages the plugin ships.
* The logo in the GuardForge screens is redrawn.
* Internal, no change in behaviour: the shared job queue names its table in every SQL statement, and the style block of the HTML e-mail carries a wordpress.org advisory mark.

Older versions are listed in changelog.txt in the plugin folder.

== Upgrade Notice ==

= 1.9.1 =
If you use the add-on, keep GuardForge Pro 1.4.0 or newer installed. The add-on requirement is unchanged.

= 1.9.0 =
If you use the add-on, keep GuardForge Pro 1.4.0 or newer installed. The add-on requirement is unchanged.

= 1.8.1 =
If you use the add-on, keep GuardForge Pro 1.4.0 or newer installed. The add-on requirement is unchanged. Sites that switch WP-Cron off no longer get the request that wakes it.

= 1.8.0 =
If you use the add-on, keep GuardForge Pro 1.4.0 or newer installed. The add-on requirement is unchanged.

= 1.7.0 =
Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before. Moving the site to another address no longer shows the audit log as tampered, and a database update that could not finish now says so.

= 1.6.5 =
Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before. Vulnerability ranges are matched again, the official Two-Factor plugin's second step is left alone, and "Verify chain" no longer cries wolf after the nightly clean-up.

= 1.6.4 =
Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before. Anonymous REST access is no longer blocked by default: only user records stay private, so the WooCommerce cart, forms and webhooks work for visitors again.

= 1.6.3 =
Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.

= 1.6.2 =
Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.

= 1.6.1 =
Deleting the last Forge plugin or add-on now removes everything Forge stored on the site, on every site of a network. Needs GuardForge Pro 1.4.0 or newer if you run the add-on, as before.

= 1.6.0 =
Needs GuardForge Pro 1.4.0 or newer if you run the add-on: the licence code and the License and Alerts screens moved into it, and an older add-on is not started beside this version until you update it. Logs, baselines and settings are untouched.

= 1.5.0 =
Settings no longer files checks that have not run yet under “Not working”, reports a switched-on monitor that stays silent, and shows rows that tie in a sort in one fixed order. If you run GuardForge Pro, it must be 1.2.0 or newer.

= 1.4.0 =
The Vulnerabilities screen now tells whether GuardForge Pro updates the database each day; the add-on's new daily update and its switch come with GuardForge Pro 1.3.0. If you run GuardForge Pro, it must be 1.2.0 or newer.

= 1.3.6 =
The new-administrator alert now fires on every way of granting the role, the REST API and add_role() included, and REST role changes are logged in full. If you run GuardForge Pro, it must be 1.2.0 or newer.

= 1.3.5 =
If you run GuardForge Pro, it must be 1.2.0 or newer.

= 1.3.4 =
The file-integrity monitor starts off and schedules nothing until you switch it on: its daily comparison asks wordpress.org for the official checksums. If you run GuardForge Pro, it must be 1.2.0 or newer.

= 1.3.3 =
Core files are no longer written by this plugin: a modified one is reported, and re-installing WordPress from Dashboard → Updates puts it back. If you run GuardForge Pro, it must be 1.2.0 or newer.

= 1.3.2 =
If you run GuardForge Pro, it must be 1.2.0 or newer; an older add-on is switched off whole until you update it.

= 1.3.1 =
If you run GuardForge Pro, it must be 1.2.0 or newer; an older add-on is switched off whole until you update it.

= 1.3.0 =
If you run GuardForge Pro, it must be 1.2.0 or newer. An older add-on never heard of staging mode and would keep mailing alerts from a clone, so it is switched off whole: alerts, the Cloudflare push, lockdown, the WAF, scanning and required 2FA stop until you update it. Logs and settings are kept.

= 1.2.0 =
Adds file integrity checked against the official wordpress.org checksums, a hash-chained audit log you can verify; a hardening score; and staging mode. The 2FA screen now opens to every logged-in role. If you run GuardForge Pro, update it to 1.2.0 too.

= 1.1.0 =
Two-factor authentication and full file-integrity monitoring are now free. If you run GuardForge Pro, update it to 1.1.0 at the same time.

= 1.0.1 =
Adds the audit log export. Nothing to do on upgrade.

= 1.0.0 =
Initial release.
