Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [5.7.0] - 2026-08-21

### Fixed

- **`setup-monitoring.yml`: the security alert e-mail had a broken subject.**
It built the subject with an unquoted `$(date +%Y-%m-%d %H:%M)`. `date`
takes `%H:%M` as a second operand and errors out, so `mail` received a
truncated subject *and* `%H:%M` as an extra recipient. The format string is
now one quoted argument.

- **`setup-monitoring.yml`: the weekly summary cron job never ran.** It was an
inline cron command containing `$(date +%Y-%m-%d)`, and cron treats an
unescaped `%` as end-of-command, piping the remainder to the job's stdin —
so the entry was truncated mid-word and wrote nothing. It now calls a
wrapper script, as the daily traffic and security reports already did, which
keeps `%` out of the crontab entirely and adds the 90-day report cleanup the
other two had.

Both were found while porting these playbooks into the standalone
[`imagewize/trellis-wp-monitoring`](https://github.com/imagewize/trellis-wp-monitoring)
Ansible role, and both failed silently — no error, just a missing report or
a mangled e-mail.

### Added

- **A Trellis command reference in the README.** The Trellis section listed
documentation links but never the commands themselves. It now opens with the
`trellis ops` equivalence and carries a generated table of all 27
`@platform trellis` commands, grouped the way `trellis ops` presents them and
marking the ones that execute on the server. The existing documentation table
moved under a "Guides" heading rather than being replaced.

## [5.6.0] - 2026-08-21

### Added
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,5 +302,5 @@ Before running backup operations on production:
### Git Commit and PR Conventions

- **Atomic commits**: Each commit should represent a single logical change. Do not bundle unrelated changes in one commit. This makes history easier to review and revert when needed.
- **AI co-authorship in commits is allowed**: `Co-Authored-By` lines for Claude or Mistral are permitted in commit messages in this repo.
- **No AI attribution in commits**: do not add `Co-Authored-By` lines for Claude or Mistral, a `Claude-Session` trailer, or any other tool reference. Commit messages carry the change and its reasoning, nothing else. This matches the PR-description rule below.
- **PR descriptions**: Pull request descriptions should be professional and focused on the changes. Do not include AI attribution footers or tool references.
92 changes: 89 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,9 +87,14 @@ trellis ops search backup

`trellis ops` scopes its **listing** to the commands tagged `@platform trellis`;
run plain `wp-ops` for the full catalog. Running a command is never scoped — name
any command and it works. Unlike core `trellis` subcommands, a plugin doesn't need
you to be inside a Trellis project; the playbook commands find the project the same
way `wp-ops` always has.
any command and it works. See the [Trellis command reference](#command-reference)
for what that surface contains.

Unlike core `trellis` subcommands, a plugin doesn't need you to be inside a Trellis
project — trellis-cli registers plugins from `$PATH` before it resolves a project at
all, so `trellis ops doctor` runs from anywhere while `trellis info` refuses. The
Ansible commands do still need a project, but they always did: wp-ops locates it
itself by walking up from your current directory, exactly as under bare `wp-ops`.

Requires trellis-cli new enough to have plugin support (v1.19.0 or later) and the
default `load_plugins: true`. If you built from source instead of installing the
Expand All @@ -110,6 +115,7 @@ no prompt to confirm a detected guess:
# Ansible playbooks (wp-ops trellis <playbook>) need a Trellis project's ansible.cfg/inventory/group_vars
export TRELLIS_DIR=/path/to/your/trellis
wp-ops trellis database-backup -e site=example.com -e env=production
trellis ops trellis database-backup -e site=example.com -e env=production # identical

# WP-CLI scripts (wp-ops wp-cli <script>) need a real WordPress/Bedrock install
export WP_SITE_DIR=/path/to/your/bedrock-site
Expand Down Expand Up @@ -153,6 +159,86 @@ cat "$(wp-ops docs -l age-verification | head -1)"

## Trellis

If you have trellis-cli, every command below is also reachable as
`trellis ops <...>` — same binary, same behaviour, no project required.
See [As a trellis-cli plugin](#as-a-trellis-cli-plugin).

```bash
trellis ops # the 27 Trellis-relevant commands
trellis ops backup database-pull example.com production
trellis ops monitoring quick-status example.com production
```

`trellis ops` scopes its *listing* to commands tagged `@platform trellis`;
running a command is never scoped, so anything in the catalog still works if
you name it. Plain `wp-ops` shows all 74.

### Command reference

Grouped as `trellis ops` presents them. `(runs on server)` marks the log
readers that execute on the host — running one locally prints the SSH
invocation instead of failing. This table is generated from the catalog;
`wp-ops list --all` is the always-current version.

**`trellis ops monitoring`** (11)

| Command | What it does |
|---------|--------------|
| `ai-bot-monitor` | Analyze AI crawler traffic (GPTBot, ClaudeBot, etc.) from an Nginx access log *(runs on server)* |
| `error-monitor` | Surface errors from Nginx, PHP-FPM, WordPress, MySQL, and systemd for a domain *(runs on server)* |
| `monitor` | Run traffic, security, AI-bot, and error monitoring together and save timestamped reports *(runs on server)* |
| `security-monitor` | Detect malicious activity (wp-login/xmlrpc abuse, high-volume IPs) in an Nginx access log *(runs on server)* |
| `traffic-by-country` | Filter a server's Nginx access log by visitor country and show real page visits |
| `traffic-monitor` | Analyze legitimate traffic from an Nginx access log *(runs on server)* |
| `updown-webhook-handler` | Analyze Nginx logs on the server when updown.io reports downtime via webhook *(runs on server)* |
| `quick-status` | Quick health check for a site: recent status codes, errors, and service status |
| `security-scan` | Scan a site's Nginx logs for attack patterns and suspicious activity |
| `setup-monitoring` | Install cron jobs for daily traffic reports and periodic security scans |
| `traffic-report` | Generate a traffic analysis report from a site's Nginx access log |

**`trellis ops backup`** (9)

| Command | What it does |
|---------|--------------|
| `db-backup` | Back up a remote site's database over SSH straight to your machine |
| `db-pull` | Pull a remote site's database into development via SSH, with URL search-replace |
| `site-backup` | Full backup of a Trellis site: database, uploads, config, and plugins/themes *(runs on server)* |
| `database-backup` | Back up a site's database from any environment (development/staging/production) |
| `database-pull` | Pull a site's database from a remote environment into development, with URL search-replace |
| `database-push` | Push development's database to a remote environment, with URL search-replace |
| `files-backup` | Back up a site's uploads directory from any environment (development/staging/production) |
| `files-pull` | Pull a site's uploads directory from a remote environment into development via rsync |
| `files-push` | Push development's uploads directory to a remote environment via rsync |

**`trellis ops content`** (2)

| Command | What it does |
|---------|--------------|
| `import-page-draft` | Update an existing WordPress page from an HTML draft, locally and/or in production |
| `page-creation` | Deploy an HTML page to production via SCP and WP-CLI over SSH |

**`trellis ops misc`** (2)

| Command | What it does |
|---------|--------------|
| `create-product-variations` | Bulk-create WooCommerce product variations via WP-CLI over Trellis vm shell |
| `trellis-updater` | Safely update a Trellis installation to the latest upstream while preserving vault/config customizations |

**`trellis ops security`** (2)

| Command | What it does |
|---------|--------------|
| `check-deny-ips` | Check every individual IP in a Trellis deny-ips.conf.j2 against AbuseIPDB |
| `check-ips` | Check IP addresses against AbuseIPDB threat intelligence |

**`trellis ops diagnostics`** (1)

| Command | What it does |
|---------|--------------|
| `list-posts-count` | Count published posts on example.com via SSH and save the list to /tmp/all_posts.csv |

### Guides

| Tool | Description | Docs |
|------|-------------|------|
| **Updater** | Safely update Trellis while preserving custom configurations | [→](trellis/updater/README.md) |
Expand Down
27 changes: 25 additions & 2 deletions trellis/monitoring/setup-monitoring.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@
# Send email only if alerts found
if grep -q "\[ALERT\]" "$REPORT_FILE"; then
if command -v mail &> /dev/null; then
mail -s "[SECURITY ALERT] {{ site }} - $(date +%Y-%m-%d %H:%M)" {{ alert_email }} < "$REPORT_FILE"
# The format string must be one quoted argument: unquoted,
# date takes %H:%M as a second operand, errors out, and mail
# gets a truncated subject plus a bogus recipient.
mail -s "[SECURITY ALERT] {{ site }} - $(date '+%Y-%m-%d %H:%M')" {{ alert_email }} < "$REPORT_FILE"
fi
fi
{% endif %}
Expand Down Expand Up @@ -141,13 +144,33 @@
user: "{{ web_user }}"
state: present

- name: Create wrapper script for weekly traffic summary
copy:
content: |
#!/bin/bash
# Weekly traffic summary for {{ site }}
#
# A wrapper rather than an inline cron command on purpose: cron
# treats an unescaped `%` as end-of-command and pipes the rest to
# the job's stdin, so a crontab line containing $(date +%Y-%m-%d)
# is silently truncated mid-word and produces nothing.
REPORT_FILE="{{ monitoring_dir }}/logs/weekly-summary-$(date '+%Y-%m-%d').txt"
{{ monitoring_dir }}/traffic-monitor.sh {{ log_file }} 168 > "$REPORT_FILE" 2>&1

# Clean up old summaries (keep last 90 days)
find {{ monitoring_dir }}/logs -name "weekly-summary-*.txt" -mtime +90 -delete
dest: "{{ monitoring_dir }}/weekly-summary.sh"
owner: "{{ web_user }}"
group: "{{ web_user }}"
mode: '0755'

- name: Setup cron job for weekly summary
cron:
name: "Weekly traffic summary for {{ site }}"
minute: "0"
hour: "9"
weekday: "1"
job: "{{ monitoring_dir }}/traffic-monitor.sh {{ log_file }} 168 > {{ monitoring_dir }}/logs/weekly-summary-$(date +%Y-%m-%d).txt 2>&1"
job: "{{ monitoring_dir }}/weekly-summary.sh"
user: "{{ web_user }}"
state: present

Expand Down