Skip to content

feat(security): WordPress file-integrity + malware scan - #54

Merged
nechodom merged 1 commit into
mainfrom
feat/integrity-scan
Aug 5, 2026
Merged

feat(security): WordPress file-integrity + malware scan#54
nechodom merged 1 commit into
mainfrom
feat/integrity-scan

Conversation

@nechodom

@nechodom nechodom commented Aug 3, 2026

Copy link
Copy Markdown
Owner

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:

  1. File integrity — wp-cli checksum verification against official WordPress.org hashes (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.
  2. Malware — ClamAV, optional, only when clamscan is 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.
  • Plugins with no published checksums (every premium plugin — Elementor Pro, ACF Pro) are reported as "not verifiable", 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 runs without --strict for the same reason.
  • Non-zero exit is data, not an error. wp-cli exits 1 when it finds something; clamscan exits 1 on a detection — those are results. Only a spawn failure is an error. clamscan exit ≥ 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_kv under integrity_scan (mirroring StoredVulnScan), 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

clamscan exit 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

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.
@nechodom
nechodom merged commit 2a6fe12 into main Aug 5, 2026
1 check passed
@nechodom
nechodom deleted the feat/integrity-scan branch August 5, 2026 15:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant