Integrity Checks
The nightly integrity check looks at the whole site, not just WordPress core. wphoster:run-integrity-checks dispatches one IntegrityCheckJob per ready site; IntegrityCheckService::run() collects findings from five sources into a single IntegrityCheckRun and raises alerts. Every tenant-touching command runs as the site's OS user (sudo -u <os_user>).
| Source | What runs | Catches |
|---|---|---|
core | wp core verify-checksums --exclude=… | Modified / missing / extra core files |
plugin | wp plugin verify-checksums --all --format=json | Modified files in wordpress.org plugins. Plugins without published checksums (Divi, ACF Pro, WPML, client plugins) are listed as unverified and covered by the manifest instead. |
theme | wp theme verify-checksums — probed per run; the installed WP-CLI build does not ship it, so summary.theme_checksums = "unavailable" and themes rely on the manifest | Modified wordpress.org themes (when available) |
manifest | Diff of disk vs file_manifests | Added / modified / removed files under plugins, themes, mu-plugins, wp-config.php, .htaccess, .user.ini, root index.php |
signature | Regexes from wphoster.integrity.signatures | Any PHP under uploads/cache/upgrade (always critical), webshell patterns in new/changed files (escalates the finding to critical) |
Flow
File manifest
file_manifests stores, per site and relative path, sha256, size, mtime, recorded_at and a reason (provision, update, agent_install, manual, accepted). It is the panel's definition of "known-good".
- Rebuild (
wphoster:rebuild-file-manifest {site}|--all) hashes every managed file and replaces the manifest. It is called automatically at the end of provisioning and cloning, after every successfulUpdateServicerun, and after installing the agent. Entries outside the managed paths (accepted PHP in uploads) survive a rebuild. - Scan lists the managed paths once with
find -printf '%s\t%T@\t%p', compares size+mtime with the manifest and hashes only the files whose metadata differs (plus new files). A file that was only touched (same hash) has its manifest metadata refreshed silently. On the 2 vCPU droplet this keeps 13 sites in the low minutes instead of hashing tens of thousands of files. - Changes whose mtime falls inside the last completed
UpdateRunwindow (±update_window_grace_minutes) are recorded withresolution=expectedand neither alert nor count as open. This is a fallback: a successful update rebuilds the manifest anyway.
Findings and resolutions
IntegrityCheckRun.failed_files is a list of findings: {source, type, file, message, severity, sha256?, size?, mtime?, signatures?, resolution?}. type is one of checksum_mismatch, missing, unexpected, added, modified, removed, php_in_forbidden_dir. summary holds per-source counts, unverified plugins and errors; open_count is the number of findings without a resolution and feeds the dashboard tile, Capacity & insights, and the promote preflight (which blocks while the production site's latest run has open findings).
- Accept as known (
POST integrity-check-runs/{run}/accept): hashes the selected files now, upserts them into the manifest withreason=accepted(or drops the row for removed files) and marks the findingsaccepted. Core/plugin checksum findings whose on-disk hash matches an accepted manifest row are auto-markedacceptedon later runs, so a deliberately patched plugin file stops nagging. - Quarantine (
POST integrity-check-runs/{run}/quarantine): copies the file tostorage/app/quarantine/<slug>/<relative path>(panel-owned, mode 0640), deletes it from the site as the OS user, records aquarantined_filesrow and drops any manifest row for it. Restore writes it back via a temp file +cpas the OS user; it does not bless the file, so the next scan reports it as added until you accept it. - Both actions and restore write
audit_events(integrity.findings_accepted,integrity.files_quarantined,integrity.file_restored).
Alerts
| Type | Raised when | Severity |
|---|---|---|
integrity_check | Open core/plugin/theme checksum findings, or the run itself errored | integrity.alert_severity (warning) |
file_change | Open manifest/signature findings | integrity.file_change_alert_severity (warning); critical if any finding is critical (PHP in uploads/cache/upgrade or a signature match) |
Both types are auto-resolved when a later run has no open findings of that kind.
Key files
| Path | Purpose |
|---|---|
app/Services/Integrity/IntegrityCheckService.php | Orchestrates the five sources, builds the run, raises/resolves alerts |
app/Services/Integrity/FileManifestService.php | Rebuild, scan, accept; update-window handling |
app/Services/Integrity/SiteFileReader.php | find / sha256sum / head / cat / rm / cp as the site user (PHP fallback when the site has no OS user) |
app/Services/Integrity/ManifestComparator.php | Pure diff: added / removed / candidates → modified / touched |
app/Services/Integrity/WebshellSignatureScanner.php | Regex matching over file contents |
app/Services/Integrity/QuarantineService.php | Quarantine / restore |
app/Console/Commands/RebuildFileManifest.php | wphoster:rebuild-file-manifest |
app/Http/Controllers/Admin/IntegrityCheckRunController.php | Index/show, accept, quarantine, restore |
app/Models/IntegrityCheckRun.php, FileManifest.php, QuarantinedFile.php | Models; IntegrityCheckRun::openFindingsTotal() powers the counters |
Config (wphoster.integrity)
| Key | Purpose |
|---|---|
exclude_files | Core paths passed to --exclude (default wp-includes/l10n.php, patched at provisioning) |
verify_plugins / verify_themes | Toggle the WP-CLI checksum steps |
manifest_paths | Relative paths covered by the manifest |
no_php_paths / php_extensions | Directories swept for executable files and which extensions count |
signatures | id → regex. Invalid regexes make wphoster:rebuild-file-manifest fail and are covered by a unit test. |
signature_max_bytes / signature_max_files | Read at most N bytes per file and scan at most N files per run (findings past the cap get signature_skipped) |
update_window_grace_minutes | Padding around the last UpdateRun when marking changes as expected |
alert_severity / file_change_alert_severity | Base severities for the two alert types |
Scheduling
- 01:00
wphoster:run-updates --staging-first— each successful update rebuilds that site's manifest. - 05:45
wphoster:rebuild-file-manifest --all --only-missing— safety net for sites with no manifest at all; never overwrites an existing one. - 06:00
wphoster:run-integrity-checks.
First deployment: run php artisan wphoster:rebuild-file-manifest --all once before the first scan, otherwise every file is reported as new. See docs/DEPLOYMENT.md.