Per-instance Moodle plugin that exposes operational health, environment details, plugin inventory, and recent config changes through Moodle web services (pull) and an optional outbound push.
Designed for fleets of Moodle sites managed by a single operator. The plugin is the data source; the central dashboard is a separate concern.
- Moodle 4.5 LTS or later (supported through 5.2)
- Web services enabled with the REST protocol
Copy the plugin to moodle/local/sentinel/, visit Site administration →
Notifications, and run the upgrade.
After install, run the bundled setup helper from the Moodle root:
php local/sentinel/cli/setup.php --username=sentinelIt enables web services + REST, creates a local_sentinel role with the
required capabilities, creates a dedicated webservice user, and prints a token
ready for the central poller to use.
GET /webservice/rest/server.php
?wstoken=<token>
&wsfunction=local_sentinel_get_snapshot
&moodlewsrestformat=json
Available functions:
| Function | Purpose |
|---|---|
local_sentinel_get_status |
Cheap liveness — version + maintenance flag |
local_sentinel_get_snapshot |
Full snapshot (all sections below) |
local_sentinel_get_environment |
PHP, OS, DB, web server, extensions |
local_sentinel_get_plugins |
Installed plugins + available updates |
local_sentinel_get_health |
Cron, tasks, sessions, disk, backups, mail |
local_sentinel_get_auth |
Enabled auth methods + user counts per method |
local_sentinel_get_reports |
Performance / Security / System status checks + MFA stats |
local_sentinel_get_config_changes |
Recent mdl_config_log entries (values of secret-named settings redacted, best-effort) |
local_sentinel_get_config_drift |
Settings whose current value differs from default (best-effort secret exclusion by class + name pattern) |
Each function returns a versioned envelope. The structure is validated
server-side via Moodle's clean_returnvalue(); auto-generated docs are
available at admin/webservice/documentation.php for the deployed token.
{
"schema_version": 3,
"generated_at": "2026-05-21T14:23:11+00:00",
"plugin": {
"component": "local_sentinel",
"version": 2026053001,
"release": "2.13.0"
},
"site": { "wwwroot": "...", "siteidentifier": "...", "sitename": "...", "shortname": "..." },
"egress": { "excluded_slices": [], "excluded_fields": [] },
"<slice_name>": { ... }
}The egress block (see Data egress filter below) declares what the site
admin has chosen to withhold, so a central dashboard can distinguish
"withheld by the site" from missing/broken data.
get_snapshot returns every slice keyed at the top level; the granular
functions (get_status, get_environment, …) return only their own slice
plus site. Snapshot slices and their high-level contents:
| Slice | Contents |
|---|---|
status |
Moodle version, branch, release, maintenance flag, branch EOL date, build age |
environment |
PHP / OS (incl. Linux distro / distro_version — the precise installed version incl. point release — from /etc/os-release) / web server / DB / OPcache / SSL / loaded extensions |
plugins |
Standard + third-party plugin lists with install status, available updates with update_available cross-reference |
health |
Cron, scheduled + adhoc tasks, sessions, disk, mail, admins (with last-changed), backup, upgrade log, foot-gun flags |
auth |
Enabled auth methods, per-method user counts, failed-login signals (locked accounts, top targets) |
reports |
Performance / Security / System status checks (via \core\check\manager) + MFA factor enrolment stats |
config_changes |
Tail of mdl_config_log (default last 50, configurable). Values of secret-named settings (smtppass, *secret, *token, …) are redacted — best-effort, name-pattern based. |
config_drift |
Settings whose current value differs from declared default (best-effort secret exclusion by class + name pattern) |
See CHANGELOG.md for schema_version history. Versioning rule:
schema_version bumps only on breaking shape changes; additive fields do
not require a bump. Central dashboards should branch parsing on
schema_version, not on the plugin's release string.
For new-client evaluation or instances behind firewalls, the plugin can push snapshots outbound on a schedule. Configure under Site administration → Plugins → Local plugins → Sentinel:
- Push endpoint URL — where to POST
- Push shared secret — sent as
X-Sentinel-Secretheader - Enable push — flip the scheduled task on
The central collector must verify the secret header before accepting the body.
A site admin can withhold data from the snapshot under Site administration → Plugins → Local plugins → Sentinel:
- Excluded slices (
egress_excluded_slices) — whole slices to omit (e.g.health,auth). - Excluded fields (
egress_excluded_fields) — dotted paths to redact within a slice (e.g.environment.database.host,environment.os.hostname).
Excluded data is removed from the payload, and the envelope's egress block
lists exactly what was withheld. This lets a central dashboard show "withheld by
the site administrator" rather than mistaking the gap for missing or broken data.
The integrity slice reports whether the Moodle code tree has been modified,
by comparing on-disk files against a pristine manifest for the site's exact
build — weekly + builds included. Manifests are derived from the public
moodle-core-manifests
dataset (reproducible from moodle.git by anyone), and are keyed by the literal
$version decimal string (integrity.core_version_full), since weekly builds
bump only the decimals.
How it works:
- Enable Core integrity scanning in the plugin settings (off by default).
- A connected dashboard pushes the matching manifest via
local_sentinel_set_manifest. The plugin never fetches anything from the internet itself — no manifest, no scan. - The weekly
integrity_scantask (or an on-demand scan queued vialocal_sentinel_request_integrity_scan) hashes the tree the way git does (sha1("blob <size>\0" + content)) and diffs it against the manifest. - Deviations ride the snapshot's
integrityslice and thelocal_sentinel_get_integrityendpoint: modified (path + expected/actual hash), missing (path), unexpected (path only — unexpected files are deliberately never hashed, so a stray secret-bearing file cannot be fingerprinted off-site). Lists are capped at 500 entries with overflow counts. A clean site reports zero counts — a few hundred bytes.
Excluded from the walk: config.php, .git, vendor, node_modules, a
nested dataroot, and the directories of installed non-standard plugins
(those are covered by the plugins slice).
Operational notes:
- The manifest POST is ~1.3 MB; the site's web server must accept request
bodies of a few MB (nginx:
client_max_body_size 4m;— the default 1 MB returns HTTP 413). - The write functions require the new
local/sentinel:managecapability. Setup (CLI or GUI) grants it to the service role; upgrading from ≤2.20 also grants it to roles that already heldlocal/sentinel:view. Manually provisioned roles need it added by hand. - The latest verdict is also shown locally at Overview → Integrity.
- The whole slice can be withheld via the egress filter like any other slice.
The plugins.updates_available and plugins.update_check fields reflect
Moodle's cached results from its last fetch of moodle.org/updates. The
plugin does not trigger a fresh fetch on every snapshot call — that would
be slow and hammer moodle.org at fleet scale.
Instead, the plugin ships its own scheduled task refresh_updates
(enabled by default, daily at a randomized time). It calls
\core\update\checker::fetch() directly — same network call as Moodle's
built-in auto-check, but without the admin email notifications.
This means you can leave Moodle's own "Check for updates and notify me" task disabled to silence the email spam, and this plugin will still keep the update cache fresh.
The task respects the top-level kill switch $CFG->disableupdatenotifications
for sites that have explicitly chosen not to phone home to moodle.org at all.
For on-demand refresh:
php local/sentinel/cli/refresh_updates.php — equivalent to clicking
"Check for available updates" at Site administration → Notifications.
Use plugins.update_check.age_seconds to judge how stale the data is.
The snapshot envelope is keyed by siteidentifier (stable across domain
migrations) and versioned via schema_version. Bump the version when adding
breaking fields; additive changes do not require a bump.
This plugin lives in the _MoodleDEV workspace. Use the workspace Makefile:
make phpcs PLUGIN=moodle-local_sentinel
make phpunit SUITE=local_sentinel_testsuite