# WordPress

> Monitor a WordPress site: beam monitor on the web server's access log with the WordPress rules, and a small must-use plugin that logs sign-ins and role changes.

Source: https://docs.agentbeam.com/cookbooks/frameworks/wordpress

WordPress needs no code in the site for requests: nginx and Apache access logs are in the combined format Beam reads, with the status. A must-use plugin adds sign-ins and role changes.

## What you'll have

- Every request checked, with WordPress user enumeration and plugin exploits flagged, and probes for other stacks rolled up.
- Sign-ins and role changes logged, so a password guessed after many failures is critical.
- A routine for reading what comes in.

## Before you start

Do steps 1 and 2 of the [general cookbook](https://docs.agentbeam.com/cookbooks/app-monitoring#step-1-turn-it-on-and-create-the-apps-token) first: App monitoring is on, the app is added, and its `beam_app_` token is in your secret store as `BEAM_APP_TOKEN`. Everything below needs `@agent-beam/beam` 0.2.32 or newer.

## Log sign-ins and role changes

```php
<?php
// wp-content/mu-plugins/beam-security.php
function beam_security_log(string $line): void {
    file_put_contents('/var/log/wordpress/beam-security.log', $line . "\n", FILE_APPEND | LOCK_EX);
}
function beam_client_ip(): string {
    return $_SERVER['REMOTE_ADDR'] ?? '';
}
add_action('wp_login', function (string $login, WP_User $user) {
    beam_security_log("login succeeded user={$user->ID} ip=" . beam_client_ip());
}, 10, 2);
add_action('wp_login_failed', function (string $username) {
    $user = get_user_by('login', $username) ?: get_user_by('email', $username);
    beam_security_log("login failed user=" . ($user ? $user->ID : '') . " ip=" . beam_client_ip());
});
add_action('set_user_role', function (int $user_id, string $role) {
    if ($role === 'administrator') beam_security_log("granted admin role to user={$user_id} by user=" . get_current_user_id());
}, 10, 2);
add_action('wp_create_application_password', function (int $user_id) {
    beam_security_log("API key created user={$user_id}");
});
```

Create `/var/log/wordpress/` writable by the PHP user and not under the web root. Behind a proxy or CDN, take the client from the header it sets, and only when the request comes from it.

## Run `beam monitor` next to the app

```sh
npm install -g @agent-beam/beam
beam monitor --stack wordpress --file /var/log/nginx/access.log --file /var/log/wordpress/beam-security.log --app /srv/app
```

`--stack wordpress` turns on this framework's rules and rolls up attacks on frameworks you don't run; `--app` also checks the app's lockfiles daily (and would detect the stack on its own). Keep it running with systemd:

```ini
# /etc/systemd/system/beam-monitor.service
[Unit]
Description=AgentBeam App monitoring
After=network-online.target

[Service]
EnvironmentFile=/etc/beam/app.env
ExecStart=/usr/bin/env beam monitor --stack wordpress --file /var/log/nginx/access.log --file /var/log/wordpress/beam-security.log --app /srv/app
Restart=always
User=beam

[Install]
WantedBy=multi-user.target
```

```sh
sudo install -m 600 /dev/null /etc/beam/app.env && echo "BEAM_APP_TOKEN=beam_app_..." | sudo tee /etc/beam/app.env >/dev/null
sudo systemctl daemon-reload && sudo systemctl enable --now beam-monitor
```

In Docker or Kubernetes, run the same command in a sidecar that mounts the log directory read-only, with `BEAM_APP_TOKEN` from a secret.
## Prove it works

```sh
beam monitor status   # app, level, stack, usage
beam monitor test     # a test finding through your alert workflows
curl -s "https://your-site.example.com/?id=1%20UNION%20SELECT%201" -o /dev/null
```

Within a minute, the SQL injection test shows on **App monitoring**: under **Needs action** if the app answered 2xx, or counted under "needed nothing from you" if it refused it. Then sign in to a test account with a wrong password ten times and the right one once: **Sign-in succeeded after 10 failed attempts** appears as critical.

## Dependencies

`beam cve check` reads `composer.lock` if you manage plugins with Composer (Bedrock, for example). Plugins installed from the dashboard have no lockfile to check: keep them updated, and remove ones you don't use.

## What Beam watches for on this stack

Besides every [built-in rule](https://docs.agentbeam.com/agent-cli/app-monitoring#levels-and-detection) and the [attack-in-progress signals](https://docs.agentbeam.com/agent-cli/app-monitoring#signs-an-attack-is-underway):

| Rule | Means | Severity |
| --- | --- | --- |
| WordPress user enumeration | `/wp-json/wp/v2/users` or `?author=1` | Medium |
| WordPress plugin file read | Known `admin-ajax.php` file-read actions (Revolution Slider, Duplicator and others) | High |
| PHP-CGI argument injection and PHPUnit | As in the [Laravel recipe](https://docs.agentbeam.com/cookbooks/frameworks/laravel#what-beam-watches-for-on-this-stack) | Critical |
| Failed sign-in bursts | Many `login failed` lines, including through `xmlrpc.php` | High |

Attacks on other frameworks (WordPress probes, Laravel or Spring exploits, and so on) are rolled up as **Not your stack** when there's no status to say otherwise, and never alert.
## Close what Beam would catch

Beam raises these when they're found; better that there's nothing to find:

- Turn off XML-RPC if nothing uses it, and limit sign-in attempts.
- Stop user enumeration (block `?author=` and restrict the users REST endpoint).
- Update core, themes and plugins; remove ones you don't use.
- Block `wp-config.php`, backups and `.git` at the web server.
## Coverage checklist

| Feature | Done when |
| --- | --- |
| Access log watched | Findings show **Answered** or **Refused** |
| Stack declared | `--stack wordpress` |
| Sign-in and role events | The must-use plugin writes to `beam-security.log` |
| Alerts | Critical pages someone, high reaches a channel ([step 8](https://docs.agentbeam.com/cookbooks/app-monitoring#step-8-send-alerts-to-the-right-place)) |
| Routine | Someone reads **Needs action** daily and **Attack analytics** weekly |
