Web Applications
Every WordPress, Drupal and other application detected in the accounts'
home directories, in one list, with the version each runs and whether a
newer release exists. It's Web Applications in the System menu, and
webapp:list on the command line. The screen answers one question during
an incident: "which installations on this server are below version X?"
The account holder's own view of the same inventory, with the buttons to update, install and sign in, is the account docs' Applications and updates. The admin app's view is read-only: to act on an installation, sign in as its account (Act in an account).
What the server does on its own
| When | What | By hand |
|---|---|---|
| Nightly, 03:41 | Scans every account's home directory for installations, and reads each WordPress site's plugin and theme inventory | webapp:scan --all --queue |
| Every 6 hours | Refreshes the upstream release feeds | webapp:releases --refresh |
| Nightly, 04:23 | Applies updates to the installations whose owners turned on automatic updates | webapp:auto-upgrade |
| Nightly, 05:17 | Polls the advisory feeds, audits every installation that can be audited, records what changed, and notifies | webapp:advisory-sweep |
| Nightly, 06:07 | Updates the plugins and themes that open advisories are against, on installations that opted in | webapp:auto-remediate |
The order carries the logic: the scan records what is installed, the feeds say what upstream ships, the advisory sweep decides what is vulnerable, and only then does remediation act. Every one of them has a command, so what the scheduler does at night can be run and watched by hand.
webapp:auto-upgrade, webapp:advisory-sweep, webapp:auto-remediate,
webapp:upgrade and webapp:remediate print what they would do and change
nothing until you pass --no-dry-run. The dry run uses the same selection
code as the real run, so what it prints is what would happen.
What is detected
The scan recognizes WordPress, Drupal, Joomla, Laravel, Symfony, Magento, Moodle, PrestaShop, MediaWiki, phpBB, Nextcloud, Ghost, Matomo, Node.js applications and PHP applications managed by Composer, each by its marker file. An installation no website serves (a backup copy, an old extract, an unpacked tarball) is listed too, marked not served: it's real code, and reachable if a document root ever points at it.
The scan is nightly, so an installation an account holder uploaded today appears tomorrow, unless you scan the account now:
elpanctl webapp:scan <account>
States
| State | Meaning |
|---|---|
| Detected | Present and readable. |
| In installation | The panel is installing it and no scan has seen the files yet. |
| Broken | The marker file is there, but the version could not be read: a truncated upload, a permission problem. Kept, because "something is here we cannot read" is worth showing. |
| Missing | A scan did not find it. The page says how many consecutive scans missed it; after three, the row is deleted. One bad scan can't drop the inventory. |
The list
The filters narrow the list before anything is read:
- Application: one type, from the types this server can resolve.
- Version below: a dotted release number, e.g.
6.9.6. Together with Application, this is the answer to an advisory: every WordPress below the fixed version. - Behind the latest release: everything below the newest release the feed knows for the selected application. It needs an application selected and a recent feed.
- State, Served, Advisories (With open advisories) and a free-text Search over title, path, host and version.
A filter that can't be applied is said in a warning, never dropped silently.
| Column | What it holds |
|---|---|
| Account | The account, linked. |
| Application | The site's title where the scan could read one, and the type. |
| Version | What the files report. |
| Update | Up to date, Update available, New major version or Unknown. |
| Advisories | The count the nightly sweep stored, and the worst severity. |
| State | The states above. |
| Served | The hostname that serves it, or not served. |
| Path | Under the account's home directory. |
| Last Seen | When a scan last observed it. |
The update status needs a release feed, and the screen names the types that have one; today those are WordPress and Drupal. Every other application is listed with its version and an unknown status. A feed that hasn't refreshed for a week also goes unknown, on purpose: the panel doesn't quote stale numbers as fact.
The Advisories column is a different question from the update status: a site at the newest release can still carry an advisory. There is no "clean only" filter, because a zero count also covers "never audited" and "the last audit failed".
An installation's page
Open a row for everything the last scan read, in one shape whatever the type: the installed version, the last action and the last upgrade with their outcome, the update status, whether the owner turned on Automatic Updates, each release on offer (a Security release is marked), the newest release and the PHP it needs, when the feed was last refreshed, the path, the website that serves it, and the database, app server and mount it uses.
Two readings cost more than a row. The file permission audit, what the web
server may write to against what the application asks for, is one
privileged call and sits on the page. The dependency audit (composer audit against the installation's lockfile, plus the open advisories
against its core) runs a process and a network call, so it's a page of its
own behind Dependencies, present only where there is something to audit.
Nothing on these pages changes a site. The opt-in to automatic updates, the permission fix, a plugin update: each is the account holder's decision, made in their panel. Mass upgrades are yours and have their own page: Upgrade Runs.
Advisories
The nightly sweep matches three sources: composer audit for the
dependencies of a resolved Composer project, OSV for an application's own
core, and Wordfence Intelligence for WordPress core and every plugin and
theme on every site, each matched on its own version.
A real run sends two mails: a digest to the server's root mailbox, grouped by advisory (System mail), and a notice to each affected account's system mailbox about its own sites. A finding is mailed once, not once per poll.
Every existing finding is "new" the first time. Record the baseline without mailing anyone, and let the following night report what changed:
elpanctl webapp:advisory-sweep --no-dry-run --no-notify
A failed reading resolves nothing: an installation that could not be audited keeps every finding it had, and a stale feed contributes nothing rather than what it last said.
elpanctl webapp:advisory-sources # each feed, when it was last polled, how old its payload is
elpanctl webapp:advisory-sources --component=wp:plugin/akismet
elpanctl webapp:audit --account=<account> # a live reading, nothing stored
elpanctl webapp:audit --type=drupal --findings-only
webapp:audit costs a Composer process and a network round trip per
installation, so narrow it with --account or --type. It exits 1 when
anything was found.
File permissions
The web server runs a site as the account's -www user and can write
nowhere unless the application declares an exception: WordPress names
wp-content/uploads, Laravel names storage/.
elpanctl webapp:permissions --type=wordpress --divergent-only
elpanctl webapp:permissions --account=<account> --apply
--apply applies the whole declaration, so a directory the account holder
opened by hand goes back to what the application asks for. Every successful
upgrade re-applies the declaration too. An absent directory is normal and is
skipped, not created.
When the status is wrong everywhere
Unknown on every row: the release feed hasn't refreshed. webapp:releases
shows each feed's freshness; --refresh fetches what is due, --force
fetches even a feed refreshed minutes ago. On a new server, server:init
fills the catalog and says which feeds it could not fetch and why; the
six-hourly refresh tries again regardless.