wpguard 0.2.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. wpguard-0.2.1/.gitignore +4 -0
  2. wpguard-0.2.1/LICENSE +21 -0
  3. wpguard-0.2.1/PKG-INFO +212 -0
  4. wpguard-0.2.1/README.fa.md +188 -0
  5. wpguard-0.2.1/README.md +184 -0
  6. wpguard-0.2.1/pyproject.toml +103 -0
  7. wpguard-0.2.1/src/wpguard/__init__.py +3 -0
  8. wpguard-0.2.1/src/wpguard/__main__.py +5 -0
  9. wpguard-0.2.1/src/wpguard/audit.py +100 -0
  10. wpguard-0.2.1/src/wpguard/backup.py +193 -0
  11. wpguard-0.2.1/src/wpguard/behavior.py +70 -0
  12. wpguard-0.2.1/src/wpguard/cli.py +263 -0
  13. wpguard-0.2.1/src/wpguard/config.py +180 -0
  14. wpguard-0.2.1/src/wpguard/dbscan.py +94 -0
  15. wpguard-0.2.1/src/wpguard/destinations.py +169 -0
  16. wpguard-0.2.1/src/wpguard/diffs.py +108 -0
  17. wpguard-0.2.1/src/wpguard/discover.py +150 -0
  18. wpguard-0.2.1/src/wpguard/feeds.py +135 -0
  19. wpguard-0.2.1/src/wpguard/fix.py +224 -0
  20. wpguard-0.2.1/src/wpguard/lockfile.py +100 -0
  21. wpguard-0.2.1/src/wpguard/monitor.py +144 -0
  22. wpguard-0.2.1/src/wpguard/recovery.py +1105 -0
  23. wpguard-0.2.1/src/wpguard/remote.py +48 -0
  24. wpguard-0.2.1/src/wpguard/report.py +66 -0
  25. wpguard-0.2.1/src/wpguard/scan.py +29 -0
  26. wpguard-0.2.1/src/wpguard/scanner.py +130 -0
  27. wpguard-0.2.1/src/wpguard/schedule.py +136 -0
  28. wpguard-0.2.1/src/wpguard/signatures.py +138 -0
  29. wpguard-0.2.1/src/wpguard/updates.py +249 -0
  30. wpguard-0.2.1/src/wpguard/util.py +56 -0
  31. wpguard-0.2.1/src/wpguard/vulns.py +32 -0
  32. wpguard-0.2.1/src/wpguard/wpcli.py +68 -0
  33. wpguard-0.2.1/src/wpguard/wpscan.py +49 -0
  34. wpguard-0.2.1/src/wpguard/yarascan.py +32 -0
  35. wpguard-0.2.1/tests/conftest.py +83 -0
  36. wpguard-0.2.1/tests/test_backup.py +242 -0
  37. wpguard-0.2.1/tests/test_behavior.py +32 -0
  38. wpguard-0.2.1/tests/test_cli.py +140 -0
  39. wpguard-0.2.1/tests/test_config.py +134 -0
  40. wpguard-0.2.1/tests/test_diffs.py +96 -0
  41. wpguard-0.2.1/tests/test_discover.py +153 -0
  42. wpguard-0.2.1/tests/test_feeds.py +76 -0
  43. wpguard-0.2.1/tests/test_fix.py +126 -0
  44. wpguard-0.2.1/tests/test_lockfile.py +63 -0
  45. wpguard-0.2.1/tests/test_monitor.py +64 -0
  46. wpguard-0.2.1/tests/test_remote.py +58 -0
  47. wpguard-0.2.1/tests/test_scanner.py +105 -0
  48. wpguard-0.2.1/tests/test_schedule.py +76 -0
  49. wpguard-0.2.1/tests/test_signatures.py +61 -0
  50. wpguard-0.2.1/tests/test_updates.py +207 -0
@@ -0,0 +1,4 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ .ruff_cache/
wpguard-0.2.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arian Omrani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
wpguard-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,212 @@
1
+ Metadata-Version: 2.5
2
+ Name: wpguard
3
+ Version: 0.2.1
4
+ Summary: Scan, clean, harden, back up and monitor hacked WordPress sites with wp-cli
5
+ Project-URL: Homepage, https://github.com/arian24b/wpguard
6
+ Project-URL: Issues, https://github.com/arian24b/wpguard/issues
7
+ Author: Arian Omrani
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: backup,incident-response,malware,security,webshell,wordpress,wp-cli
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: System Administrators
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Security
18
+ Classifier: Topic :: System :: Systems Administration
19
+ Requires-Python: >=3.14
20
+ Provides-Extra: all
21
+ Requires-Dist: boto3>=1.34; extra == 'all'
22
+ Requires-Dist: pymysql>=1.1; extra == 'all'
23
+ Provides-Extra: mysql
24
+ Requires-Dist: pymysql>=1.1; extra == 'mysql'
25
+ Provides-Extra: s3
26
+ Requires-Dist: boto3>=1.34; extra == 's3'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # wpguard
30
+
31
+ [![CI](https://github.com/arian24b/wpguard/actions/workflows/ci.yml/badge.svg)](https://github.com/arian24b/wpguard/actions/workflows/ci.yml)
32
+
33
+ **English** | [فارسی](https://github.com/arian24b/wpguard/blob/main/README.fa.md)
34
+
35
+ Command line tool to scan, clean, harden, back up and monitor hacked WordPress sites, built on [wp-cli](https://wp-cli.org/). Python 3.14, no runtime dependencies (`boto3` for S3 and `pymysql` for `recover` are optional extras).
36
+
37
+ ## Install
38
+
39
+ ```bash
40
+ uvx wpguard --help # run without installing (`uvx --from wpguard wpg` for the short name) (uv downloads Python 3.14 if needed)
41
+ uv tool install wpguard # or: pipx install wpguard
42
+ pip install 'wpguard[s3]' # S3 backups (boto3) | 'wpguard[mysql]' for `recover` against a live MySQL
43
+ wpg --help # `wpg` is a short alias of `wpguard` (installed together)
44
+ wpguard setup # downloads wp-cli (sha512 verified) and creates ~/.config/wpguard/wpguard.toml
45
+ wpguard init # (optional) starter ./wpguard.toml for this project
46
+ ```
47
+
48
+ Requirements: Linux, `php` and a MySQL/MariaDB client for the site commands (not for `baseline`, `watch`, `logs`, `verify`). Run as the **site's file owner** (`sudo -u www-data wpguard ...`) so new files keep the right ownership. Optional tools it uses when present: `yara` (or YARA-X `yr`), `clamscan`, `age`, `rsync`, `ssh`, `wpscan`/docker.
49
+
50
+ ## Quick start
51
+
52
+ ```bash
53
+ wpguard scan /var/www/site # 1. look first, changes nothing
54
+ wpguard fix /var/www/site --dry-run # 2. see the full plan
55
+ wpguard fix /var/www/site --url https://your.site # 3. does it (asks to confirm; --yes to skip)
56
+ wpguard undo /var/www/site # false positive? put quarantined files back
57
+ wpguard harden /var/www/site --lockdown --url https://your.site # after everything is updated
58
+ wpguard baseline /var/www/site # snapshot the clean state
59
+ wpguard schedule add watch /var/www/site --every 15m --install # alert on any change
60
+ ```
61
+
62
+ `SITE` can be a path, a **profile name** from `wpguard.toml`, a **hostname** (`blog.example.com`), or **`host:/path`** / **`host:domain`** to run it over ssh (see below).
63
+
64
+ ## Commands
65
+
66
+ | Command | What it does |
67
+ |---|---|
68
+ | `scan` | read-only: signatures, hash DBs, YARA, behavior score, `wp-config.php`/`.htaccess`/`.user.ini` tampering, hidden files, mu-plugins, ClamAV, core/plugin checksum extras, DB injections, new admins, odd cron, outdated/abandoned/vulnerable plugins |
69
+ | `fix` | scan → **plan** → confirm → backup → quarantine → reinstall core/plugins/themes (pinned versions if locked) → DB clean → harden → new salts → rescan → baseline. `--dry-run` stops after the plan |
70
+ | `undo` | move quarantined files back (latest quarantine, `--from DIR`, `--only GLOB`) |
71
+ | `harden` | block PHP in uploads, apply wp-config constants, perms 755/644; `--prune` removes inactive plugins/themes |
72
+ | `diff` | unified diff of every modified core/plugin file against the official wordpress.org file |
73
+ | `backup` | DB dump + all files → `wpguard-backup-<site>-<time>.tar.zst`, optional encryption, concurrent upload to several destinations, retention, verification |
74
+ | `verify` | check a backup (checksum, readable archive, plausible `db.sql`, WordPress files present) |
75
+ | `restore` | re-import the DB and files from a backup (checksum verified first) |
76
+ | `baseline` / `watch` | SHA-256 snapshot / diff against it; exit 1 and alert on new, changed or removed files |
77
+ | `lock` | write `wpguard.lock` (pinned core/plugin/theme versions and feed digests); `lock --check` reports drift |
78
+ | `updates` | try every pending update on a **staging copy**, report which ones break the site; `--apply` applies the safe ones |
79
+ | `logs` | find the entry point in access logs: brute force, xmlrpc, exploit POSTs, requests to shells you quarantined |
80
+ | `audit` | full inventory + findings as Markdown or JSON (secrets redacted) |
81
+ | `sigs list` / `sigs update` | signature feeds (maldet hashes, YARA rule sets) |
82
+ | `schedule add\|remove\|show JOB SITE` | cron lines or systemd user timers for `watch backup scan sigs updates` |
83
+ | `wp [--unsafe] SITE ARGS...` | run **any** wp-cli command (plugins/themes not loaded unless `--unsafe`) |
84
+ | `wpscan [URL] ARGS...` | run the real [WPScan](https://wpscan.com/) (local binary or docker); `WPSCAN_TOKEN` becomes `--api-token` |
85
+ | `recover ARGS...` | rebuild a wiped site from the DB or a dump (`recover --help`) |
86
+ | `setup` | download wp-cli and create `~/.config/wpguard/wpguard.toml` if no config exists |
87
+ | `init` | create a commented starter `./wpguard.toml` (or `--config FILE`; `--force` overwrites) |
88
+ | `discover [HOST...]` | find WordPress sites by hostname on this machine or ssh hosts; `--save` adds them to the config |
89
+
90
+ ## Config file and profiles
91
+
92
+ `wpguard setup` (global, `~/.config/wpguard/wpguard.toml`) or `wpguard init` (this directory) writes a commented starter file; `wpguard discover --save` fills in sites for you. It is searched in `./` then `~/.config/wpguard/`, or use `--config FILE`:
93
+
94
+ ```toml
95
+ [defaults]
96
+ since = 14
97
+ keep = 7
98
+
99
+ [sites.blog]
100
+ path = "/var/www/blog"
101
+ url = "https://blog.example.com"
102
+ to = ["/srv/backups", "s3://my-bucket/blog", "rsync:backup:/srv/backups/blog"]
103
+
104
+ [sites.mina]
105
+ ssh = "mina" # a host from ~/.ssh/config
106
+ path = "/var/www/mina" # path on the remote host
107
+ ```
108
+
109
+ Now `wpguard scan blog`, `wpguard backup --all`, `wpguard fix mina --dry-run` work. Any CLI option can be a config key (long name, dashes → underscores). Precedence: command line > `[sites.NAME]` > `[defaults]`. Unknown keys are an error. `[notify]` holds `telegram_token`, `telegram_chat`, `email` (env vars `WPGUARD_TELEGRAM_TOKEN`, `WPGUARD_TELEGRAM_CHAT`, `WPGUARD_EMAIL` win; keep the file private).
110
+
111
+ ### wpguard.lock
112
+
113
+ `wpguard lock blog` records core, plugin and theme versions (and the digests of downloaded signature feeds) in `wpguard.lock` next to the config; commit it. Then:
114
+
115
+ - `fix` reinstalls the **pinned** versions instead of "latest" (no surprise major jumps while cleaning),
116
+ - `lock --check` exits 1 when something drifted (new plugin, version changed, feed changed): a good cron job,
117
+ - `updates --apply` refreshes the pins for the updates it applied.
118
+
119
+ ## Sites by hostname
120
+
121
+ Any site command accepts the site's **hostname** instead of its path:
122
+
123
+ ```bash
124
+ wpguard discover # list WordPress sites on this machine (nginx/apache vhosts + wp-config.php)
125
+ wpguard discover mina --save # same on the ssh host `mina`, and add them to wpguard.toml
126
+ wpguard scan blog.example.com # by hostname (profile `domain`/`url`, or found from this machine's vhosts)
127
+ wpguard backup mina:blog.example.com # hostname on an ssh host: path is looked up over ssh, then the command runs there
128
+ ```
129
+
130
+ Resolution order for `SITE`: profile name → profile `domain`/`url` host → `host:/path` → `host:domain` → an existing local path → a bare domain looked up in this machine's web-server configs. A profile may omit `path` when it has a `domain` (and optionally `ssh`): the path is then found from the vhosts. Discovery reads `/etc/nginx`, `/etc/apache2`, `/etc/httpd` (`server_name`/`ServerName`/`ServerAlias` + `root`/`DocumentRoot`) and `wp-config.php` under `/var/www /srv /home /opt`; unusual layouts can still be registered by hand. `www.` variants match.
131
+
132
+ ## Remote sites over SSH
133
+
134
+ ```bash
135
+ wpguard scan mina:/var/www/site # ad-hoc: host from ~/.ssh/config, path on the remote
136
+ wpguard scan mina # profile with ssh = "mina"
137
+ wpguard backup mina --to s3://bkt/mina --pull ./pulled # remote backup, archive rsync'ed back
138
+ ```
139
+
140
+ wpguard runs the same command on the remote machine through `ssh <host> <remote_cmd> ...`, so your `~/.ssh/config` (keys, ProxyJump, ports) is used as-is. The remote needs `uv` (the default `remote_cmd` is `uvx wpguard`; set `remote_cmd = "~/.local/bin/wpguard"` per profile if it is installed). Your CLI options and config values are forwarded; `--config`, `--report`, `-j`, `--pull` stay local. Exit codes propagate (1 = findings). Paths in options such as `--hashdb` are read **on the remote host**.
141
+
142
+ ## Detection
143
+
144
+ - **Signatures** (built in) and `--sigs FILE` (your own regexes; blank/invalid lines are ignored).
145
+ - **Hash databases**: `--hashdb` known-bad, `--hashdb-good` known-clean (suppresses findings). Any file with hex md5/sha1/sha256: csv, txt, json, `.gz`, ClamAV `.hdb`, SQLite.
146
+ - **Behavior score** (`--behavior-threshold`, default 5; `--no-behavior`): dangerous-call density, hex/chr obfuscation, high entropy, very long lines, double extensions, PHP in asset dirs, names used by known malware, mtime far newer than siblings or in the future. **Review only**: `fix` does not quarantine behavior or YARA findings unless you pass `--aggressive`.
147
+ - **YARA**: `--yara RULES.yar` (repeatable) through the `yara` or `yr` binary.
148
+ - **Feeds**: `wpguard sigs update` downloads maldet md5 hashes ([rfxn](https://www.rfxn.com/projects/linux-malware-detect/)), [php-malware-finder](https://github.com/nbs-system/php-malware-finder) (LGPL-3.0) and [Neo23x0/signature-base](https://github.com/Neo23x0/signature-base) webshell rules (Detection Rule License 1.1: attribution). They are cached under `~/.local/share/wpguard/feeds`, used automatically by `scan`/`fix` (`--no-feeds` disables) and their digests are pinned by `lock`. Add your own with `[feeds.NAME]` in the config.
149
+ - **Core/plugin diff**: `wpguard diff SITE [--core] [--plugin SLUG]` shows what an attacker changed in an official file.
150
+ - `--ignore GLOB` (repeatable, or `ignore = [...]` in config) silences known-good paths.
151
+
152
+ ## Safer fix
153
+
154
+ `fix` always prints the plan (what will be quarantined, what is *only* flagged for review, which users will be deleted, which versions will be installed) and then asks `Proceed? [y/N]`. Non-interactive runs refuse unless `--yes`; `-j > 1` requires `--yes` or `--dry-run`. Quarantined files are **moved** (never deleted) into `<site parent>/wpguard-quarantine/<site>-<time>/files/` with a `manifest.json`; `wpguard undo` puts them back. `wp-config.php` and `.htaccess` outside uploads are only ever reported. A backup (without uploads) is taken before anything changes.
155
+
156
+ ## Backups
157
+
158
+ ```bash
159
+ wpguard backup blog # to the destinations in the profile (or next to the site)
160
+ wpguard backup /var/www/site --out /srv/backups --verify --keep 7
161
+ wpguard backup blog --to s3://bkt/blog --to rsync:backup:/srv/b --encrypt-to age1... --keep 14
162
+ wpguard verify blog --out /srv/backups # latest local backup
163
+ wpguard restore /var/www/site --from /srv/backups/wpguard-backup-site-20261009-113206.tar.zst
164
+ ```
165
+
166
+ Archive layout: `db.sql` + `site/...` in a zstd tar, mode 600, with a `.sha256` sidecar; an existing backup is never overwritten. **Destinations** (`--to`, repeatable): a local directory, `s3://bucket/prefix` (`boto3`, standard `AWS_*` env, `AWS_ENDPOINT_URL` for MinIO/R2), `rsync:HOST:/path` (HOST may be an ssh alias). All destinations upload **concurrently**; each is size-checked after upload and one failing destination does not stop the others (exit 1). **Retention** (`--keep N`) keeps the newest N per destination and site, and only runs after a verified upload. `--verify` checks the archive before uploading. `--encrypt-to` needs [`age`](https://age-encryption.org); restore/verify of such files needs `--age-identity KEY`. `--no-uploads` leaves out `wp-content/uploads`. The archive contains `wp-config.php` (DB password): keep it private.
167
+
168
+ ## Update advisor
169
+
170
+ `wpguard updates SITE` lists pending core/plugin/theme updates and tests **each one** on a throwaway copy: files (without uploads) plus a cloned database, served by PHP's built-in server on 127.0.0.1. After every update it requests `/`, `/wp-login.php`, `/wp-admin/` (add more with `--check-url`) and reads the server log; 5xx pages and PHP fatals are reported as `BREAKS`, then that plugin is rolled back and the next one is tested alone. Inside the copy outgoing mail and HTTP (except wordpress.org) are blocked and WP-Cron is off, so loading the site's plugins cannot e-mail customers. Needs `php` and a MySQL user that may `CREATE DATABASE` (or `--stage-db EXISTING_EMPTY_DB`). `--apply` backs up and then updates only the items that passed. `--keep-stage` leaves the copy for inspection.
171
+
172
+ ## Scheduler
173
+
174
+ ```bash
175
+ wpguard schedule show backup blog --daily 03:30 # prints the cron line
176
+ wpguard schedule add watch blog --every 15m --install # writes it to your crontab (idempotent, marker comments)
177
+ wpguard schedule add backup blog --daily 03:30 --systemd --install # systemd user timer
178
+ wpguard schedule remove watch blog
179
+ ```
180
+
181
+ Jobs: `watch backup scan sigs updates`; `watch`, `scan` and `updates` run with `--notify`. Put alert settings in `[notify]` (cron has no environment). For systemd user timers to run while logged out: `loginctl enable-linger $USER`.
182
+
183
+ ## Other guides
184
+
185
+ **Cleaning a hacked site**: isolate the site → `scan --since 14` → `logs` (find the hole: POSTs to plugin PHP, requests to files you later quarantine) → `fix --dry-run`, then `fix` → reinstall premium plugins marked `SKIP` from the vendor → change DB/FTP/SSH/WordPress passwords and remove unknown admins → `harden --lockdown` after updating → `baseline` + `schedule add watch`.
186
+
187
+ **Wiped site**: `wpguard recover --dump site.sql --db NAME --user U --out site --list` (inventory), then without `--list` to download core/plugins/themes at the versions the DB records; restore uploads and premium code from a backup and continue as above.
188
+
189
+ **Audit**: `wpguard audit blog --out blog.md` (or `.json`): versions, settings, plugins, themes, users by role, cron hooks, mu-plugins, drop-ins, `wp-config.php` constants with DB user/password/salts redacted, plus all findings.
190
+
191
+ ## Development
192
+
193
+ ```bash
194
+ uv sync
195
+ uv run ruff check . && uv run ruff format --check . # ruff, select = ["ALL"] (ignores are listed with reasons in pyproject.toml)
196
+ uv run pytest
197
+ uv build
198
+ ```
199
+
200
+ GitHub Actions (`.github/workflows/ci.yml`) runs ruff, pytest and a wheel smoke test on every push and pull request. When CI succeeds on `main`, `release.yml` runs [python-semantic-release](https://python-semantic-release.readthedocs.io/): it reads [Conventional Commits](https://www.conventionalcommits.org/) (`feat:` → minor, `fix:` → patch, `feat!:` / `BREAKING CHANGE:` → major), bumps `__version__` and `uv.lock`, updates `CHANGELOG.md`, tags `vX.Y.Z`, creates the GitHub release and publishes the built package to PyPI (trusted publishing: add a publisher for this repo, workflow `release.yml`, environment `pypi` on pypi.org once). Commits without a releasable type (`docs:`, `chore:`, `ci:`, `test:`) do not release.
201
+
202
+ ## What it does NOT do
203
+
204
+ - Detection is heuristic. Obfuscated commercial plugins can be flagged and new shells can be missed; it does not replace a WAF or server-level scanning.
205
+ - DB cleaning is regex-based: read the dry run. `fix` installs pinned versions if locked, otherwise the **latest** (a major jump can break compatibility; `updates` exists to test that).
206
+ - The update advisor needs a real PHP + MySQL environment; DB migrations done by a plugin update can leave the staging DB changed between candidates.
207
+ - Remote mode requires `uv` (or an installed `wpguard`) on the remote host.
208
+ - Quarantine and backups sit next to the site (outside the web root); delete them when no longer needed, they hold old malicious files and database dumps.
209
+
210
+ ## Safety and license
211
+
212
+ Use only on sites you own or are authorised to administer. MIT license (`LICENSE`). Third-party signature feeds keep their own licenses (see above).
@@ -0,0 +1,188 @@
1
+ <div dir="rtl">
2
+
3
+ # wpguard
4
+
5
+ [![CI](https://github.com/arian24b/wpguard/actions/workflows/ci.yml/badge.svg)](https://github.com/arian24b/wpguard/actions/workflows/ci.yml)
6
+
7
+ [English](README.md) | **فارسی**
8
+
9
+ ابزار خط فرمان برای اسکن، پاکسازی، سخت‌سازی، بکاپ و پایش سایت‌های وردپرسی هک‌شده، بر پایه [wp-cli](https://wp-cli.org/). پایتون ۳.۱۴، بدون وابستگی اجباری (`boto3` برای S3 و `pymysql` برای `recover` اختیاری‌اند).
10
+
11
+ ## نصب
12
+
13
+ ```bash
14
+ uvx wpguard --help # اجرا بدون نصب (uv خودش پایتون ۳.۱۴ را می‌گیرد)
15
+ uv tool install wpguard # یا: pipx install wpguard
16
+ pip install 'wpguard[s3]' # بکاپ روی S3 | 'wpguard[mysql]' برای recover روی MySQL زنده
17
+ wpg --help # `wpg` نام کوتاه wpguard است (همراه آن نصب می‌شود)
18
+ wpguard setup # دانلود wp-cli (با بررسی sha512) و ساخت ~/.config/wpguard/wpguard.toml
19
+ wpguard init # (اختیاری) ساخت ./wpguard.toml نمونه برای همین پروژه
20
+ ```
21
+
22
+ پیش‌نیاز: لینوکس، `php` و کلاینت MySQL/MariaDB برای دستورهای سایت (نه برای `baseline`، `watch`، `logs`، `verify`). با **کاربر مالک فایل‌های سایت** اجرا کنید (`sudo -u www-data wpguard ...`). ابزارهای اختیاری که در صورت وجود استفاده می‌شوند: `yara` (یا `yr`)، `clamscan`، `age`، `rsync`، `ssh`، `wpscan`/docker.
23
+
24
+ ## شروع سریع
25
+
26
+ ```bash
27
+ wpguard scan /var/www/site # ۱. اول فقط ببینید، چیزی تغییر نمی‌کند
28
+ wpguard fix /var/www/site --dry-run # ۲. طرح کامل را ببینید
29
+ wpguard fix /var/www/site --url https://your.site # ۳. اجرا (تأیید می‌گیرد؛ --yes برای رد کردن)
30
+ wpguard undo /var/www/site # false positive بود؟ فایل‌های قرنطینه را برگردانید
31
+ wpguard harden /var/www/site --lockdown --url https://your.site # بعد از به‌روزرسانی کامل
32
+ wpguard baseline /var/www/site # عکس‌برداری از وضعیت تمیز
33
+ wpguard schedule add watch /var/www/site --every 15m --install # هشدار با هر تغییر
34
+ ```
35
+
36
+ `SITE` می‌تواند مسیر، **نام پروفایل** در `wpguard.toml`، **نام میزبان (hostname)** مثل `blog.example.com` یا **`host:/path`** / **`host:domain`** (اجرا از طریق ssh) باشد.
37
+
38
+ ## دستورها
39
+
40
+ | دستور | کار |
41
+ |---|---|
42
+ | `scan` | فقط‌خواندنی: امضا، پایگاه هش، YARA، امتیاز رفتاری، دست‌کاری `wp-config.php`/`.htaccess`/`.user.ini`، فایل‌های مخفی، mu-plugins، ClamAV، فایل‌های اضافه نسبت به checksum، تزریق در دیتابیس، ادمین جدید، کرون مشکوک، افزونه قدیمی/رهاشده/آسیب‌پذیر |
43
+ | `fix` | اسکن ← **طرح** ← تأیید ← بکاپ ← قرنطینه ← نصب مجدد هسته/افزونه/پوسته (نسخه‌های قفل‌شده اگر باشد) ← پاکسازی دیتابیس ← سخت‌سازی ← salt جدید ← اسکن مجدد ← baseline. با `--dry-run` بعد از طرح متوقف می‌شود |
44
+ | `undo` | برگرداندن فایل‌های قرنطینه‌شده (آخرین قرنطینه، `--from DIR`، `--only GLOB`) |
45
+ | `harden` | مسدود کردن PHP در uploads، ثابت‌های wp-config، دسترسی 755/644؛ `--prune` موارد غیرفعال را حذف می‌کند |
46
+ | `diff` | diff یکپارچه هر فایل تغییریافته هسته/افزونه با فایل رسمی wordpress.org |
47
+ | `backup` | dump دیتابیس + همه فایل‌ها ← `wpguard-backup-<site>-<time>.tar.zst`، رمزگذاری اختیاری، آپلود هم‌زمان به چند مقصد، نگهداری (retention)، راستی‌آزمایی |
48
+ | `verify` | بررسی بکاپ (checksum، آرشیو خوانا، `db.sql` معقول، وجود فایل‌های وردپرس) |
49
+ | `restore` | بازگردانی دیتابیس و فایل‌ها از بکاپ (ابتدا checksum بررسی می‌شود) |
50
+ | `baseline` / `watch` | عکس SHA-256 / مقایسه با آن؛ با فایل جدید، تغییریافته یا حذف‌شده کد خروج ۱ و هشدار |
51
+ | `lock` | نوشتن `wpguard.lock` (نسخه‌های قفل‌شده هسته/افزونه/پوسته و digest فیدها)؛ `lock --check` انحراف را گزارش می‌دهد |
52
+ | `updates` | هر به‌روزرسانی در انتظار را روی **نسخه staging** امتحان می‌کند و می‌گوید کدام سایت را خراب می‌کند؛ `--apply` موارد امن را اعمال می‌کند |
53
+ | `logs` | یافتن راه نفوذ از روی لاگ: brute force، xmlrpc، POST به افزونه‌ها، درخواست به شل‌های قرنطینه‌شده |
54
+ | `audit` | موجودی کامل + یافته‌ها به صورت Markdown یا JSON (اطلاعات محرمانه حذف می‌شود) |
55
+ | `sigs list` / `sigs update` | فیدهای امضا (هش‌های maldet، قوانین YARA) |
56
+ | `schedule add\|remove\|show JOB SITE` | خط cron یا تایمر systemd برای `watch backup scan sigs updates` |
57
+ | `wp [--unsafe] SITE ARGS...` | اجرای **هر** دستور wp-cli (افزونه‌ها و پوسته‌ها بارگذاری نمی‌شوند مگر با `--unsafe`) |
58
+ | `wpscan [URL] ARGS...` | اجرای خود [WPScan](https://wpscan.com/) (برنامه محلی یا docker)؛ `WPSCAN_TOKEN` به‌صورت `--api-token` اضافه می‌شود |
59
+ | `recover ARGS...` | بازسازی سایت پاک‌شده از دیتابیس یا dump (`recover --help`) |
60
+ | `setup` | دانلود wp-cli و ساخت `~/.config/wpguard/wpguard.toml` اگر config وجود نداشته باشد |
61
+ | `init` | ساخت `./wpguard.toml` نمونه با کامنت (یا `--config FILE`؛ `--force` بازنویسی می‌کند) |
62
+ | `discover [HOST...]` | پیدا کردن سایت‌های وردپرس با hostname روی همین ماشین یا هاست‌های ssh؛ `--save` آن‌ها را به config اضافه می‌کند |
63
+
64
+ ## فایل پیکربندی و پروفایل‌ها
65
+
66
+ `wpguard setup` (سراسری، `~/.config/wpguard/wpguard.toml`) یا `wpguard init` (همین پوشه) یک فایل نمونه با کامنت می‌سازد و `wpguard discover --save` سایت‌ها را خودش اضافه می‌کند. فایل در `./` سپس `~/.config/wpguard/` جست‌وجو می‌شود، یا `--config FILE`:
67
+
68
+ ```toml
69
+ [defaults]
70
+ since = 14
71
+ keep = 7
72
+
73
+ [sites.blog]
74
+ path = "/var/www/blog"
75
+ url = "https://blog.example.com"
76
+ to = ["/srv/backups", "s3://my-bucket/blog", "rsync:backup:/srv/backups/blog"]
77
+
78
+ [sites.mina]
79
+ ssh = "mina" # یک host از ~/.ssh/config
80
+ path = "/var/www/mina" # مسیر روی سرور راه دور
81
+ ```
82
+
83
+ حالا `wpguard scan blog`، `wpguard backup --all` و `wpguard fix mina --dry-run` کار می‌کنند. هر گزینه CLI می‌تواند کلید config باشد (نام بلند، خط تیره ← زیرخط). اولویت: خط فرمان > `[sites.NAME]` > `[defaults]`. کلید ناشناس خطا است. `[notify]` شامل `telegram_token`، `telegram_chat`، `email` است (متغیرهای محیطی `WPGUARD_TELEGRAM_TOKEN`، `WPGUARD_TELEGRAM_CHAT`، `WPGUARD_EMAIL` اولویت دارند؛ فایل را خصوصی نگه دارید).
84
+
85
+ ### wpguard.lock
86
+
87
+ `wpguard lock blog` نسخه‌های هسته، افزونه‌ها و پوسته‌ها (و digest فیدهای دانلودشده) را در `wpguard.lock` کنار config ثبت می‌کند؛ آن را commit کنید. سپس:
88
+
89
+ - `fix` به‌جای «آخرین نسخه»، **نسخه‌های قفل‌شده** را نصب می‌کند (بدون جهش ناگهانی نسخه هنگام پاکسازی)،
90
+ - `lock --check` هنگام انحراف (افزونه جدید، تغییر نسخه، تغییر فید) کد خروج ۱ می‌دهد: برای cron عالی است،
91
+ - `updates --apply` قفل موارد به‌روزشده را تازه می‌کند.
92
+
93
+ ## سایت با hostname
94
+
95
+ هر دستور سایت به‌جای مسیر، **نام میزبان** سایت را هم می‌پذیرد:
96
+
97
+ ```bash
98
+ wpguard discover # فهرست سایت‌های وردپرس همین ماشین (vhost های nginx/apache + wp-config.php)
99
+ wpguard discover mina --save # همین کار روی هاست ssh به نام mina و افزودن به wpguard.toml
100
+ wpguard scan blog.example.com # با hostname (از domain/url پروفایل، یا از vhost های همین ماشین)
101
+ wpguard backup mina:blog.example.com # hostname روی هاست ssh: مسیر از طریق ssh پیدا می‌شود و دستور همان‌جا اجرا می‌شود
102
+ ```
103
+
104
+ ترتیب تشخیص `SITE`: نام پروفایل ← host مربوط به `domain`/`url` پروفایل ← `host:/path` ← `host:domain` ← مسیر محلیِ موجود ← دامنه‌ای که در تنظیمات وب‌سرور همین ماشین پیدا شود. اگر پروفایل `domain` داشته باشد می‌تواند `path` نداشته باشد (و اختیاراً `ssh`)؛ مسیر از روی vhost ها پیدا می‌شود. Discovery فایل‌های `/etc/nginx`، `/etc/apache2`، `/etc/httpd` (`server_name`/`ServerName`/`ServerAlias` به‌علاوه `root`/`DocumentRoot`) و `wp-config.php` زیر `/var/www /srv /home /opt` را می‌خواند؛ چینش‌های غیرعادی را می‌توان دستی ثبت کرد. نسخه‌های `www.` هم تطبیق داده می‌شوند.
105
+
106
+ ## سایت‌های راه دور با SSH
107
+
108
+ ```bash
109
+ wpguard scan mina:/var/www/site # موردی: host از ~/.ssh/config، مسیر روی سرور راه دور
110
+ wpguard scan mina # پروفایل با ssh = "mina"
111
+ wpguard backup mina --to s3://bkt/mina --pull ./pulled # بکاپ راه دور، آرشیو با rsync برمی‌گردد
112
+ ```
113
+
114
+ wpguard همان دستور را از طریق `ssh <host> <remote_cmd> ...` روی ماشین راه دور اجرا می‌کند، پس `~/.ssh/config` شما (کلید، ProxyJump، پورت) همان‌طور استفاده می‌شود. سرور راه دور به `uv` نیاز دارد (پیش‌فرض `remote_cmd` برابر `uvx wpguard` است؛ اگر نصب شده، در پروفایل `remote_cmd = "~/.local/bin/wpguard"` بگذارید). گزینه‌ها و مقادیر config فرستاده می‌شوند؛ `--config`، `--report`، `-j`، `--pull` محلی می‌مانند. کد خروج منتقل می‌شود (۱ = یافته). مسیرهای گزینه‌هایی مثل `--hashdb` **روی سرور راه دور** خوانده می‌شوند.
115
+
116
+ ## تشخیص
117
+
118
+ - **امضاهای** داخلی و `--sigs FILE` (regex خودتان؛ خط خالی/نامعتبر نادیده گرفته می‌شود).
119
+ - **پایگاه هش**: `--hashdb` مخرب، `--hashdb-good` سالم (یافته را حذف می‌کند). هر فایلی با هش hex از نوع md5/sha1/sha256: csv، txt، json، `.gz`، `.hdb` مربوط به ClamAV، SQLite.
120
+ - **امتیاز رفتاری** (`--behavior-threshold` پیش‌فرض ۵؛ `--no-behavior`): تراکم فراخوانی‌های خطرناک، مبهم‌سازی hex/chr، آنتروپی بالا، خطوط بسیار بلند، پسوند دوگانه، PHP در پوشه‌های asset، نام‌های بدافزارهای شناخته‌شده، mtime بسیار جدیدتر از هم‌پوشه‌ای‌ها یا در آینده. **فقط بازبینی**: `fix` یافته‌های behavior و YARA را قرنطینه نمی‌کند مگر با `--aggressive`.
121
+ - **YARA**: `--yara RULES.yar` (قابل تکرار) از طریق برنامه `yara` یا `yr`.
122
+ - **فیدها**: `wpguard sigs update` هش‌های md5 مجموعه maldet ([rfxn](https://www.rfxn.com/projects/linux-malware-detect/))، [php-malware-finder](https://github.com/nbs-system/php-malware-finder) (LGPL-3.0) و قوانین webshell مجموعه [Neo23x0/signature-base](https://github.com/Neo23x0/signature-base) (Detection Rule License 1.1: ذکر منبع) را می‌گیرد. در `~/.local/share/wpguard/feeds` کش می‌شوند، خودکار در `scan`/`fix` استفاده می‌شوند (`--no-feeds` غیرفعال می‌کند) و digest آن‌ها با `lock` قفل می‌شود. فید خودتان را با `[feeds.NAME]` در config اضافه کنید.
123
+ - **diff هسته/افزونه**: `wpguard diff SITE [--core] [--plugin SLUG]` نشان می‌دهد مهاجم در فایل رسمی چه تغییری داده است.
124
+ - `--ignore GLOB` (قابل تکرار، یا `ignore = [...]` در config) مسیرهای سالم را ساکت می‌کند.
125
+
126
+ ## fix امن‌تر
127
+
128
+ `fix` همیشه اول طرح را چاپ می‌کند (چه چیزی قرنطینه می‌شود، چه چیزی *فقط برای بازبینی* علامت خورده، کدام کاربران حذف می‌شوند، چه نسخه‌هایی نصب می‌شود) و بعد `Proceed? [y/N]` می‌پرسد. اجرای غیرتعاملی بدون `--yes` رد می‌شود؛ `-j > 1` به `--yes` یا `--dry-run` نیاز دارد. فایل‌های قرنطینه **جابه‌جا** می‌شوند (هرگز حذف نمی‌شوند) به `<پوشه والد سایت>/wpguard-quarantine/<site>-<time>/files/` همراه `manifest.json`؛ `wpguard undo` آن‌ها را برمی‌گرداند. `wp-config.php` و `.htaccess` بیرون از uploads فقط گزارش می‌شوند. پیش از هر تغییر یک بکاپ (بدون uploads) گرفته می‌شود.
129
+
130
+ ## بکاپ
131
+
132
+ ```bash
133
+ wpguard backup blog # به مقصدهای پروفایل (یا کنار سایت)
134
+ wpguard backup /var/www/site --out /srv/backups --verify --keep 7
135
+ wpguard backup blog --to s3://bkt/blog --to rsync:backup:/srv/b --encrypt-to age1... --keep 14
136
+ wpguard verify blog --out /srv/backups # آخرین بکاپ محلی
137
+ wpguard restore /var/www/site --from /srv/backups/wpguard-backup-site-20261009-113206.tar.zst
138
+ ```
139
+
140
+ ساختار آرشیو: `db.sql` + `site/...` در tar با zstd، دسترسی 600، همراه `.sha256`؛ بکاپ موجود هرگز بازنویسی نمی‌شود. **مقصدها** (`--to`، قابل تکرار): پوشه محلی، `s3://bucket/prefix` (با `boto3`، متغیرهای معمول `AWS_*`، و `AWS_ENDPOINT_URL` برای MinIO/R2)، `rsync:HOST:/path` (HOST می‌تواند نام مستعار ssh باشد). آپلود به همه مقصدها **هم‌زمان** انجام می‌شود؛ پس از آپلود اندازه بررسی می‌شود و خرابی یک مقصد بقیه را متوقف نمی‌کند (کد خروج ۱). **Retention** (`--keep N`) جدیدترین N بکاپ هر مقصد و سایت را نگه می‌دارد و فقط پس از آپلود تأییدشده اجرا می‌شود. `--verify` آرشیو را پیش از آپلود بررسی می‌کند. `--encrypt-to` به [`age`](https://age-encryption.org) نیاز دارد؛ restore/verify چنین فایل‌هایی به `--age-identity KEY` نیاز دارد. `--no-uploads` پوشه `wp-content/uploads` را کنار می‌گذارد. آرشیو `wp-config.php` (رمز دیتابیس) دارد: خصوصی نگهش دارید.
141
+
142
+ ## مشاور به‌روزرسانی
143
+
144
+ `wpguard updates SITE` به‌روزرسانی‌های در انتظار هسته/افزونه/پوسته را فهرست می‌کند و **هر کدام** را روی یک کپی موقت امتحان می‌کند: فایل‌ها (بدون uploads) + دیتابیس کلون‌شده، با سرور داخلی PHP روی 127.0.0.1. بعد از هر به‌روزرسانی `/`، `/wp-login.php`، `/wp-admin/` (با `--check-url` بیشتر کنید) را درخواست می‌کند و لاگ سرور را می‌خواند؛ صفحه 5xx و خطای fatal پی‌اچ‌پی `BREAKS` گزارش می‌شود، سپس همان افزونه برگردانده شده و مورد بعدی جداگانه امتحان می‌شود. در این کپی ایمیل خروجی و HTTP خروجی (جز wordpress.org) مسدود و WP-Cron خاموش است، پس بارگذاری افزونه‌ها نمی‌تواند به مشتری ایمیل بزند. به `php` و کاربر MySQL با اجازه `CREATE DATABASE` نیاز دارد (یا `--stage-db` یک دیتابیس خالی موجود). `--apply` ابتدا بکاپ می‌گیرد و فقط موارد موفق را به‌روز می‌کند. `--keep-stage` کپی را برای بررسی نگه می‌دارد.
145
+
146
+ ## زمان‌بند
147
+
148
+ ```bash
149
+ wpguard schedule show backup blog --daily 03:30 # فقط خط cron را چاپ می‌کند
150
+ wpguard schedule add watch blog --every 15m --install # در crontab شما می‌نویسد (idempotent، با کامنت نشانه)
151
+ wpguard schedule add backup blog --daily 03:30 --systemd --install # تایمر کاربر systemd
152
+ wpguard schedule remove watch blog
153
+ ```
154
+
155
+ Jobها: `watch backup scan sigs updates`؛ `watch`، `scan` و `updates` با `--notify` اجرا می‌شوند. تنظیمات هشدار را در `[notify]` بگذارید (cron متغیر محیطی ندارد). برای اجرای تایمرهای کاربر systemd وقتی لاگین نیستید: `loginctl enable-linger $USER`.
156
+
157
+ ## راهنماهای دیگر
158
+
159
+ **پاکسازی سایت هک‌شده**: سایت را ایزوله کنید ← `scan --since 14` ← `logs` (راه نفوذ: POST به PHP افزونه‌ها، درخواست به فایل‌هایی که بعداً قرنطینه می‌کنید) ← اول `fix --dry-run` سپس `fix` ← افزونه‌های پولی با برچسب `SKIP` را از سایت سازنده نصب کنید ← رمز دیتابیس/FTP/SSH/وردپرس را عوض و ادمین‌های ناشناس را حذف کنید ← بعد از به‌روزرسانی `harden --lockdown` ← `baseline` و `schedule add watch`.
160
+
161
+ **سایت پاک‌شده**: `wpguard recover --dump site.sql --db NAME --user U --out site --list` (فقط فهرست)، سپس بدون `--list` برای دانلود هسته/افزونه/پوسته با نسخه‌های ثبت‌شده در دیتابیس؛ uploads و کدهای پولی را از بکاپ برگردانید و ادامه دهید.
162
+
163
+ **audit**: `wpguard audit blog --out blog.md` (یا `.json`): نسخه‌ها، تنظیمات، افزونه‌ها، پوسته‌ها، کاربران به تفکیک نقش، کرون، mu-plugins، drop-ins، ثابت‌های `wp-config.php` بدون رمز/نام کاربری دیتابیس/saltها، و همه یافته‌ها.
164
+
165
+ ## توسعه
166
+
167
+ ```bash
168
+ uv sync
169
+ uv run ruff check . && uv run ruff format --check . # ruff با select = ["ALL"] (موارد ignore با دلیل در pyproject.toml)
170
+ uv run pytest
171
+ uv build
172
+ ```
173
+
174
+ GitHub Actions (`.github/workflows/ci.yml`) با هر push و pull request ابزار ruff، pytest و یک smoke test از wheel را اجرا می‌کند. وقتی CI روی `main` موفق شود، `release.yml` ابزار [python-semantic-release](https://python-semantic-release.readthedocs.io/) را اجرا می‌کند: [Conventional Commits](https://www.conventionalcommits.org/) را می‌خواند (`feat:` ← minor، `fix:` ← patch، `feat!:` یا `BREAKING CHANGE:` ← major)، `__version__` و `uv.lock` را بالا می‌برد، `CHANGELOG.md` را به‌روز می‌کند، تگ `vX.Y.Z` و GitHub release می‌سازد و پکیج ساخته‌شده را روی PyPI منتشر می‌کند (trusted publishing: یک بار در pypi.org یک publisher برای این repo، workflow به نام `release.yml` و environment به نام `pypi` اضافه کنید). commitهای بدون نوع قابل‌انتشار (`docs:`، `chore:`، `ci:`، `test:`) نسخه جدید نمی‌سازند.
175
+
176
+ ## محدودیت‌ها
177
+
178
+ - تشخیص heuristic است. افزونه‌های تجاری مبهم‌سازی‌شده ممکن است false positive بدهند و شل‌های جدید ممکن است جا بمانند؛ جایگزین WAF یا اسکن سطح سرور نیست.
179
+ - پاکسازی دیتابیس مبتنی بر regex است: dry-run را بخوانید. `fix` اگر قفل باشد نسخه‌های قفل‌شده، وگرنه **آخرین** نسخه را نصب می‌کند (جهش نسخه بزرگ ممکن است ناسازگاری ایجاد کند؛ `updates` برای امتحان همین است).
180
+ - مشاور به‌روزرسانی به محیط واقعی PHP + MySQL نیاز دارد؛ مهاجرت دیتابیس که توسط به‌روزرسانی افزونه انجام می‌شود ممکن است دیتابیس staging را بین کاندیداها تغییر دهد.
181
+ - حالت راه دور به `uv` (یا `wpguard` نصب‌شده) روی سرور مقصد نیاز دارد.
182
+ - قرنطینه و بکاپ‌ها کنار سایت (بیرون از وب‌روت) هستند؛ وقتی لازم نیستند پاکشان کنید، فایل مخرب قدیمی و dump دیتابیس دارند.
183
+
184
+ ## ایمنی و مجوز
185
+
186
+ فقط روی سایت‌هایی استفاده کنید که مالک آن‌ها هستید یا مجوز مدیریتشان را دارید. مجوز MIT (`LICENSE`). فیدهای امضای شخص ثالث مجوز خودشان را دارند (بالا را ببینید).
187
+
188
+ </div>