feat(security): WordPress file-integrity + malware scan - #54
Merged
Conversation
The care package we're about to sell promises "pravidelná kontrola na
přítomnost malwaru a virů". Hyperion could not do that. Its only "scan"
(wp_vuln_scan) detects OUTDATED plugins/themes — a different claim
entirely, and one that would not catch a site that is already compromised.
This makes the promise true.
Two independent signals, both keyless, matching the defender's existing
no-API-keys stance:
1. FILE INTEGRITY via wp-cli's checksum verification against official
WordPress.org hashes (`wp core verify-checksums`, `wp plugin
verify-checksums`). This catches the actual common compromise: injected
or modified core/plugin PHP. Core findings are split into three buckets
because wp-cli reports them as distinct things — modified, should-not-
exist, and missing.
2. MALWARE via ClamAV, OPTIONAL and only when `clamscan` is present.
clamd is heavy for a shared host, so a missing binary means "not
available", never an error and never a false clean.
The honesty rules are the whole point of the feature, so they are enforced
in the type rather than left to the UI:
* `is_clean()` returns true ONLY when both signals actually ran. A site
whose wp-cli or ClamAV could not run can never render as a green tick —
telling a paying customer their site is fine when nothing was checked
is the one failure this feature must not have.
* Plugins with NO published checksums (every premium plugin — Elementor
Pro, ACF Pro) are reported as "not verifiable", kept structurally and
visually separate from plugins that FAILED verification. Conflating
them would cry wolf on every real site and train the operator to ignore
the panel. Verification deliberately runs without `--strict` for the
same reason.
* A non-zero exit from wp-cli or clamscan is DATA (findings exist), not
an error; only a spawn failure is an error. clamscan exit >= 2 (some
files unreadable) reports "couldn't check", not "clean".
Parsers are pure and total — 17 unit tests over real-shaped fixtures
including modified core, extra files, curly-apostrophe wp-cli output,
premium plugins, colon-bearing paths, and garbage input.
Per-hosting result is stored in hosting_kv under `integrity_scan`,
mirroring StoredVulnScan, with a tick that sweeps active WordPress sites
and a lazily-loaded panel on the hosting page with a Scan now button.
834 tests pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
The care package we're about to sell promises "pravidelná kontrola na přítomnost malwaru a virů". Hyperion could not do that. Its only "scan" (
wp_vuln_scan) detects outdated plugins/themes — a different claim, and one that would not notice a site that is already compromised. This makes the promise true.Two signals, both keyless
Matching the defender's existing no-API-keys stance:
wp core verify-checksums,wp plugin verify-checksums). Catches the actual common compromise: injected/modified core or plugin PHP. Core findings split into three buckets, because wp-cli reports them as genuinely different things: modified, should-not-exist, missing.clamscanis present. clamd is heavy for a shared host, so a missing binary means not available — never an error, never a false clean.The honesty rules are the feature
These are enforced in the type, not left to the UI:
is_clean()is true only when both signals actually ran. A site whose wp-cli or ClamAV could not run can never render as a green tick. Telling a paying customer their site is fine when nothing was checked is the one failure this feature must not have.--strictfor the same reason.clamscanexit ≥ 2 (files unreadable) reports couldn't check, not clean.Shape
Parsers are pure and total — 17 unit tests over real-shaped fixtures: modified core, extra file in wp-includes, missing core file, curly-apostrophe wp-cli output, premium plugins landing in unknown not failed, colon-bearing paths, empty/garbage input. None can panic.
Per-hosting result lives in
hosting_kvunderintegrity_scan(mirroringStoredVulnScan), with a tick sweeping active WordPress sites and a lazily-loaded panel + Scan now button on the hosting page. Result reuses the vuln-scan architecture throughout — same shapes, same storage idiom, same owner-node dispatch.Tests
834 pass (+29), fmt clean.
Known judgement call
clamscanexit 2 currently reports couldn't check. In the field that exit often just means a few unreadable files (broken symlinks) on a busy docroot, which could make the malware signal look permanently unavailable. Erring toward honesty for now; if field data shows exit 2 is routine, the fix is to return the parsed hits alongside a soft error — not to downgrade exit 2 to clean.🤖 Generated with Claude Code