Skip to content
KitploitKITPLOIT
StrumentiBlog
Invia
StrumentiBlog
Invia

Strumenti di Hacking, PenTest e Cybersecurity per il tuo Arsenale di Sicurezza!

Kitploit è una directory di strumenti di hacking, cybersecurity e pentesting. Scopri gli ultimi aggiornamenti dei progetti per trovare vulnerabilità, analizzare sistemi, automatizzare i test e rafforzare la tua sicurezza.

··Feed·Contatto·Privacy·© 2026 Kitploit

Directory degli strumenti

Categorie

Vedi tutte le categorie
Loading categories
Strumenti/GitLabGitLab/jp.louvel/ncstatuscheck
Vulnerability AnalysisConfiguration AuditingCloud Security
GitLabjp.louvel/ncstatuscheck

NcStatusCheck

Monitoring centralisé pour instances Nextcloud multiples

Vedi Repository
2141h 10m faNon ancora revisionato

Più Popolari

Vedi tutti →

Scopri gli strumenti più utilizzati dalla nostra community.

Esplora tutti gli strumenti

Sfoglia la nostra collezione di strumenti

Vedi tutti gli strumenti →
Condividi
Contenuto non disponibile nella lingua richiesta. Visualizzazione della versione inglese.

NcStatusCheck

Centralized monitoring for multiple Nextcloud instances

NcStatusCheck is a monitoring tool that lets you track the health of multiple Nextcloud servers from a single web interface. It analyzes Nextcloud and PHP versions and provides update recommendations.

NcStatusCheck Interface NcStatusCheck Interface - 2

🎯 Features

Monitoring

  • Automated surveillance of multiple Nextcloud instances
  • Three data collection modes per server: Basic (NC version only), Extended (full data via serverinfo API), and Push (data sent by the remote instance)
  • Version analysis for Nextcloud and PHP
  • Web server detection (nginx, Apache) and HTTP protocols
  • Up/down tracking (mini uptime): current reachability + last state change, surfaced as a red "offline" badge only when down. A bounded transition journal also feeds a 30-day availability % and a recent-incident list on the detail page
  • SSL certificate expiry alert: read from the existing HTTPS probe (no extra request), badge when the certificate expires soon (< 15d / < 7d)
Scarica lo strumento
  • Installed apps audit: cross-checks occ app:list against the Nextcloud app store to flag apps to review — blocking (upgrade-blocking, incompatible, test apps in production) plus informational maturity/turbulence signals (recently published, pre-1.0, alpha/beta/rc build, burst of releases, freshly released version)
  • Fleet-wide watch blocks: an "Apps to watch" (📦) and a twin "Containers to watch" (🐳) block aggregate every flagged app / Docker image across all instances into one entry each, with per-group type filters and a popup listing the impacted instances and their versions
  • Proactive alerts (webhook and/or email): fires on a confirmed up/down state change (offline, maintenance, recovered) so you are notified without a dashboard open — Slack / Mattermost / Google Chat / Discord / raw JSON (ALERT_WEBHOOK_URL), and/or a digest email per batch (ALERT_EMAIL_TO) sent by direct SMTP submission to your mail server (vendored PHPMailer; falls back to the local MTA when no SMTP relay is configured). Also covers slow-moving signals (ALERT_CHECKS): SSL certificate expiry (tiered), vulnerable/deprecated Nextcloud version, stale Push probe, critical audit report, blocking app finding — one alert per new condition, no reminder spam
  • Update grace delay: an instance is only flagged outdated a few days after a patch is released (nc_update_grace_days), to avoid chasing a same-day-buggy release
  • Health column using the "silence = all good" principle: only shows badges (⚠️ warnings, 📦 apps, 🔄 Docker, 🔒 SSL, 🔴 offline, 🚧 maintenance) when there is something to report
  • Cache system for server data and official versions
  • Detail page

    • Full view of each Nextcloud server's configuration (PHP, OPcache, Redis, database…) for Extended/Push servers
    • Basic detail page for servers without a probe: shows version, web server, HTTP protocol, and an upgrade hint
    • 30-day availability % and recent-incident list (start, end, duration, reason), computed from the bounded up/down transition journal — honest "measured over N days" label while the journal is younger than the window
    • Configuration warning rules with ⚠️ indicators, each acknowledgeable (reason + optional expiry)
    • Server audit report section when the instance pushes its monthly nc-audit.sh report
    • Accessible by clicking on any server name or a health indicator

    Interface

    • Centralized dashboard with status overview (summary cards surface offline / expiring-cert / apps-to-review counts only when non-zero)
    • Advanced filters by Nextcloud status, PHP status, overall status, running Docker image, and text search
    • Official versions automatically fetched from Nextcloud sources
    • Release schedule with upcoming version dates
    • FR / EN / DE interface (language switcher in the top-right corner)

    Administration

    • Administration interface to configure version rules
    • Probe modes summary table at the top: explains the three collection methods (Basic, Extended, Push) at a glance
    • Server management via web UI with card layout: two distinct probe zones (serverinfo / push), masked tokens with reveal, inline delete
    • "Configure probes" button per server to toggle token zones, state saved in localStorage
    • Real-time server search with accent-insensitive matching
    • Push script generator: generates a ready-to-use bash cron script (chmod 700) for the remote Nextcloud instance, auto-updated as options change
    • "Trigger Push" button on the dashboard: sends a push request to all configured push servers (visible only when at least one push server exists, 5-minute cooldown)
    • Flexible configuration of security thresholds per Nextcloud branch

    🏗️ Architecture

    root@kitploit:~
    ncstatuscheck/
    ├── Frontend
    │   ├── index.php              # Main entry point
    │   ├── template.html          # HTML template (dashboard)
    │   ├── admin.html             # Administration interface
    │   ├── detail.php             # Server detail page
    │   ├── audit.php              # Server-audit script distribution page (nc-audit.sh)
    │   ├── troubleshooting.php    # Probe troubleshooting guide
    │   ├── app.js / admin.js / detail.js / audit.js / troubleshooting.js
    │   └── style*.css             # One stylesheet per page family
    ├── APIs (HTTP)
    │   ├── api.php                # Main monitoring API
    │   ├── detail-api.php         # Detail page API (serverinfo + warnings + acks + availability)
    │   ├── push-api.php           # Push reception + trigger + audit-report reception
    │   ├── ack-api.php            # Warning acknowledge / unmute
    │   ├── admin-api.php          # Version configuration routing
    │   ├── servers-admin-api.php  # Server list management
    │   ├── nextcloud-versions-api.php # Official version scraping
    │   ├── nextcloud-apps-api.php # App store catalog (slim cache) for the apps audit
    │   ├── desktop-releases-api.php # Desktop client releases (GitHub, slim cache)
    │   ├── php-versions-api.php   # PHP branch support data
    │   └── apps-warnings-api.php  # Manual app warnings (known-bug list) CRUD
    ├── Shared modules (lib/)
    │   ├── auth.php               # Auth + CSRF + URL redaction (defense in depth)
    │   ├── csrf-client.js         # Auto-inject X-CSRF-Token in fetch()
    │   ├── nextcloud-client.php   # Centralized HTTP client → remote Nextclouds
    │   ├── servers-store.php      # Single source of truth for servers.json
    │   ├── uptime-state.php       # Up/down state machine + transition journal + availability
    │   ├── alerts.php             # Proactive alert dispatch: webhook + email digest
    │   ├── alerts-checks.php      # Slow-signal alerts (SSL/version/push/audit/apps) + dedup state
    │   ├── smtp-mailer.php        # SMTP transport adapter over vendored PHPMailer
    │   ├── phpmailer/             # Vendored PHPMailer (3 files + LICENSE, pinned in VERSION)
    │   ├── apps-warnings-manager.php # Manual app warnings storage
    │   ├── ui-common.js           # NcUI: notify / confirm / prompt + shared app-audit messages
    │   ├── url-guard.php          # Anti-SSRF (loopback, RFC1918, link-local…)
    │   ├── json-cache.php         # Locked JSON read/write helpers
    │   └── version-config-manager.php # Version rules CRUD
    ├── Business logic
    │   ├── version-rules.php      # NC / PHP status analysis engine
    │   ├── warnings-rules.php     # Configuration warning engine
    │   ├── apps-rules.php         # Installed-apps audit engine (store catalog cross-check)
    │   ├── advisories-rules.php   # Security advisory normalisation (pure: version bounds, tag→version)
    │   ├── advisories-api.php     # Advisory upstream clients + slim cache (no HTTP entry point)
    │   ├── lib/advisories-fleet.php # Fleet <-> advisory matching (I/O wrapper over the pure rules)
    │   ├── cron-update.php        # Full collection script, CLI only (twice a day)
    │   ├── cron-ping.php          # Lightweight up/down probe, CLI only (every 5 min)
    │   └── cron-advisories.php    # Security advisory collection, CLI only (daily, opt-in)
    ├── Tools (never web-served — blocked by nginx/.htaccess)
    │   ├── tools/nc-audit.sh      # Standalone server audit script (root, read-only)
    │   └── tools/ncstatuscheck-push-core.sh # Generic Push probe core (fleet-shared)
    ├── Tests
    │   └── tests/run.php          # Plain-PHP test suite (no framework): php tests/run.php
    ├── Configuration
    │   ├── config.php             # Central configuration (git-ignored)
    │   └── servers.json           # Server list with tokens (git-ignored)
    └── Cache
        ├── servers_data.json      # All server data
        ├── serverinfo_<md5>.json  # Raw per-server cache (Extended)
        ├── push_<md5>.json        # Last push payload per server
        ├── ack_<md5>.json         # Acknowledged warnings per server
        ├── audit_<md5>.json       # Last nc-audit.sh report per server
        ├── version-config.json    # Version configuration
        ├── uptime_state.json      # Up/down state per server (mini uptime)
        ├── uptime_history.json    # Bounded up/down transition journal (availability % + incidents)
        ├── alerts_state.json      # "Already alerted" memory of the check alerts
        ├── nextcloud_versions.json # Official NC versions
        ├── nextcloud_apps.json    # App store slim catalog (apps audit)
        ├── apps-warnings.json     # Manual app warnings (admin-curated)
        ├── .csrf_secret           # CSRF HMAC secret (binary, 0600)
        └── *.log                  # Activity logs
    
    deploy/ansible/                # Fleet deployment of the Push core (Ansible / scp)
    deploy/docker/                 # Container packaging of the monitor itself
    

    🔌 Collection modes

    Modes are not mutually exclusive — a server can be Extended and Push simultaneously.

    ModeBadgeSourceCollected data
    Basic(none)/status.php + HTTP headersNextcloud version (PHP/webserver if exposed)
    Extended⚡ Extended (purple → orange on error/stale)/ocs/v2.php/apps/serverinfo/api/v1/info with NC-TokenNC version, PHP, web server, OPcache, Redis, DB, active users…
    Push📡 Push (blue → orange on error/stale)POST to push-api.phpData pushed by the remote NC instance via cron script

    The serverinfo NC-Token is available in Nextcloud Settings → Administration → System.

    The push token is generated from the administration interface; the admin provides a ready-to-use bash cron script (chmod 700) to deploy on the monitored instance.

    Extended mode data is provided by the nextcloud/serverinfo app, which must be installed and enabled on the monitored instance.

    Fallback behavior: if the Extended API is unreachable (connection error, invalid token, app not installed), NcStatusCheck automatically falls back to /status.php to retrieve at least the Nextcloud version.

    Push staleness threshold: a push server is considered stale if no data has been received within auto_push_interval + 30 minutes. The default push interval is 12 hours.

    Dashboard table columns

    The main dashboard shows 5 columns: Server | NC Version | PHP | Probes | Health

    The Probes column displays the active collection modes for each server:

    • ⚡ Extended badge (purple, turns orange on connection error or stale data)
    • 📡 Push badge (blue, turns orange when no data received within the threshold)
    • Both badges can appear simultaneously if both modes are active
    • No badge = Basic mode only

    Health column

    The Health column only shows something when there is something to act on:

    IndicatorBadgeMeaning
    Offline🔴 OfflineInstance unreachable (HTTP probe failed), with "offline for X"
    Active warnings⚠️ NN configuration issues
    Apps audit📦 NN installed apps to review (upgrade-blocking/incompatible)
    SSL expiry🔒 N dCertificate expires soon — orange < 15d, red < 7d or expired
    Docker updates🔄 MM container updates available
    All OK(empty)Nothing to report
    No data?Basic mode with no push data

    Up/down & SSL expiry

    NcStatusCheck keeps a minimal up/down state per server (current state + last change date only — no time series, no history page). "Up" means the outbound HTTPS probe reached the instance; a red Offline badge appears only when down. During that same HTTPS probe, the SSL certificate expiry is read for free (CURLOPT_CERTINFO) and surfaced when it gets close. Both are visible in full on the detail page. Note: these outbound checks don't apply to Push-only instances that the monitor never contacts.

    Apps audit (📦)

    When a Push server reports its installed apps (occ app:list, push script v3+), NcStatusCheck cross-checks them against the Nextcloud app store catalogue and flags apps worth reviewing. Signalling only — the tool never disables anything; it surfaces candidates (it cannot know whether an app is actually used). Only factual, binary signals are used. The 📦 N badge counts blocking findings (no compatible release for the current NC version, no release for NC N+1 → blocks the upgrade, or a test/dev app left enabled in production). Informational findings (app outdated on the instance, abandoned upstream, PHP incompatible) are shown on the detail page only. Findings can be muted via the same acknowledge mechanism as warnings.

    servers.json format

    root@kitploit:~
    [
      {"url": "https://cloud.example.com"},
      {"url": "https://cloud2.example.com", "serverinfo_token": "abc123def456"},
      {"url": "https://cloud3.example.com", "serverinfo_token": "...", "push_token": "xyz789"}
    ]
    

    🚀 Installation

    Prerequisites

    • PHP 8.1+ with cURL and JSON extensions — that's the syntax floor CI tests against, not a deployment recommendation: 8.1 and 8.2 are both already past their security-support end date, use a currently-supported version (8.3+ as of this writing) for anything internet-facing
    • Web server nginx or Apache with HTTPS
    • Network access to the Nextcloud servers to monitor

    Web server configuration (nginx)

    root@kitploit:~
    server {
        server_name monitoring.your-domain.com;
        root /var/www/ncstatuscheck;
        index index.php;
    
        # HTTP Basic Authentication
        auth_basic "Monitoring Access";
        auth_basic_user_file /etc/nginx/.htpasswd;
    
        # Protect sensitive files/dirs (tests/run.php has no CLI-only guard — it must
        # never be reachable over HTTP; same blocklist as deploy/docker/nginx.conf)
        location ~ ^/(cache/|\.git|deploy/|tools/|tests/) {
            deny all;
            return 404;
        }
    
        # .txt covers servers.txt (legacy server list — real monitored URLs)
        location ~* \.(log|json|txt)$ {
            deny all;
            return 404;
        }
    
        # Security headers for static HTML pages (admin.html, template.html).
        # PHP pages (index.php, detail.php) send the same headers themselves
        # via send_security_headers() in lib/auth.php.
        location ~* \.html$ {
            add_header X-Content-Type-Options nosniff always;
            add_header X-Frame-Options DENY always;
            add_header Referrer-Policy no-referrer always;
            add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'" always;
            try_files $uri =404;
        }
    
        # Standard PHP configuration (adjust the socket to your PHP version —
        # use a security-supported one: 8.2 has been EOL since December 2025)
        location ~ \.php$ {
            fastcgi_pass unix:/run/php/php8.4-fpm.sock;
            fastcgi_index index.php;
            include fastcgi_params;
            fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        }
    
        location / {
            try_files $uri $uri/ =404;
        }
    }
    

    Apache: the repository ships .htaccess files mirroring the deny rules above (root: blocks *.log/*.json/*.txt and .git; cache/, tools/, deploy/, tests/: Require all denied). They only work if the vhost sets AllowOverride FileInfo AuthConfig (or All) — Debian's default for /var/www is AllowOverride None, in which case replicate the rules in the vhost directly. HTTP Basic auth still has to be configured in the vhost either way.

    Deployment

    1. Clone the repository
    root@kitploit:~
    git clone https://gitlab.com/jp.louvel/ncstatuscheck.git
    cd ncstatuscheck
    
    1. Configuration Create your configuration file from the template:
    root@kitploit:~
    cp config-example.php config.php
    

    Edit config.php and adjust paths and URL to your environment:

    root@kitploit:~
    define('MONITOR_PATH', '/var/www/ncstatuscheck');
    define('MONITOR_URL', 'https://monitoring.your-domain.com'); // your public URL
    define('CACHE_DIR', MONITOR_PATH . '/cache');
    
    1. Add servers

    Servers are managed directly from the administration interface (⚙️ Admin button). You can also create servers.json manually:

    root@kitploit:~
    [
      {"url": "https://cloud.example.com"},
      {"url": "https://nextcloud.mycompany.org", "serverinfo_token": "your_token_here"}
    ]
    

    Migration from servers.txt: if a servers.txt file exists, it is automatically converted to servers.json on first access. You can then delete servers.txt.

    1. Set permissions

    nginx/PHP-FPM run as their own user (www-data on Debian/Ubuntu, nginx/apache on RHEL-family — adjust below) — if you cloned as your own login user, that user almost certainly won't be www-data or in its group, so chmod alone leaves the web server with no access at all, not even read (every request 403/404s):

    root@kitploit:~
    chown -R www-data:www-data /var/www/ncstatuscheck   # adjust the user:group to your distro
    
    chmod 750 /var/www/ncstatuscheck
    chmod 750 cache img
    
    chmod 640 *.php *.html *.js *.css *.md
    chmod 600 config.php servers.json servers.txt   # secrets / serverinfo & push tokens
    chmod 660 cache/*.json cache/*.log
    

    If servers.json doesn't exist yet (you're letting the admin UI create it instead of the manual step above), the chmod 600 above simply has nothing to act on — that's fine: ServersStore::save() chmods the file to 0600 itself on every write, so a servers.json (re)created through the admin UI never stays group/world-readable with serverinfo/push tokens inside.

    1. HTTP authentication
    root@kitploit:~
    htpasswd -c /etc/nginx/.htpasswd admin
    
    1. Scheduled tasks (optional)

    Two complementary crons — install both in the same crontab - call: crontab - installs a whole new crontab from stdin, it doesn't append, so running it twice (once per line) leaves only the second job — the first silently vanishes, no error. This also preserves anything already in your crontab (crontab -l piped in first) instead of wiping it:

    root@kitploit:~
    (crontab -l 2>/dev/null; cat <<'EOF'
    # Full collection (NC/PHP versions, serverinfo, app-store catalog) — twice a day
    0 6,18 * * * cd /var/www/ncstatuscheck && php cron-update.php
    # Lightweight reachability probe (status.php only -> up/down state) — every 5 min
    */5 * * * * cd /var/www/ncstatuscheck && php cron-ping.php
    # Security advisories (opt-in, see below) — once a day
    30 4 * * * cd /var/www/ncstatuscheck && php cron-advisories.php
    EOF
    ) | crontab -
    

    Re-running this appends duplicates if these lines are already present — check with crontab -l first if unsure.

    cron-ping.php is intentionally minimal: it only checks each instance's status.php and updates the up/down state (cache/uptime_state.json), so it can run frequently without load. An instance is only flagged down after UPTIME_FAIL_THRESHOLD consecutive failed probes (default 2 → ~10 min with a 5-min cadence); recovery to up is immediate. The full cron-update.php remains unchanged for everything else.

    Security advisory collection (cron-advisories.php, opt-in)

    Collects the published security advisories of the three products this tool can see running on a fleet — Nextcloud, Collabora and OnlyOffice — into cache/advisories.json.

    It is a collector only. Nothing in the web interface reads that cache yet: turning it on changes what the cron writes, and nothing else. It is off by default, so an existing installation keeps behaving exactly as before.

    root@kitploit:~
    // config.php
    define('ADVISORIES_ENABLED', true);
    // Optional, defaults to all three:
    define('ADVISORIES_SOURCES', 'nextcloud,collabora,onlyoffice');
    
    root@kitploit:~
    30 4 * * * cd /var/www/ncstatuscheck && php cron-advisories.php
    

    No page ever triggers a fetch. Anonymous GitHub allows 60 requests/h per IP and NVD 5 per 30 s; letting a dashboard load spend that budget would be an outage inflicted on ourselves. Collection happens from this cron and nowhere else — cron-update.php does not call it either.

    Sources. Nextcloud advisories come from the nextcloud/security-advisories repository (server and apps — nextcloud/server publishes an empty list), Collabora from CollaboraOnline/online, both through the GitHub repository-advisories API. OnlyOffice has no usable GitHub advisories and is read from the NVD 2.0 API by CPE.

    Options

    OptionEffect
    --dry-runPrint the JSON on stdout and write nothing. Progress goes to stderr, so the output pipes into jq. Works even when ADVISORIES_ENABLED is false — it is how you inspect the result before switching it on.
    --source=IDRefresh a single source (nextcloud, collabora, onlyoffice). The others are carried over from the existing cache, untouched and unmarked.
    --verboseEcho every log line. Without it, only warnings and errors reach the console — a cron that prints on a good day trains you to ignore its mail.
    --reportMeasure the fleet against the cached advisories and print the result. Reads local caches only — no network, no write, no lock — and works whether or not collection is enabled.

    Exit codes: 0 every selected source refreshed · 1 partial success, at least one source failed · 2 total failure, or the cache could not be written. For --report: 0 nothing found · 1 at least one match · 2 no advisory cache, or no collected server data — "we cannot tell" never exits like "all clean".

    What --report measures

    The Nextcloud version is available for every server, Basic mode included, because /status.php carries it. Collabora and OnlyOffice versions come from container tags, so they only exist for instances in Push mode — which is also where the gain is largest, since nothing else in this tool can currently say more than "a newer tag exists" about those two.

    Each product lands in one of four states, and they are never collapsed: clean, affected, unknown (covered product, version not determinable — a floating tag; must never read as up to date) and out_of_scope (redis, postgres, mariadb… — an answer, and a different one from "no CVE").

    The branch question is the whole problem. An advisory lists one entry per maintained branch, so "am I below the fix" is only meaningful against the entry for my branch. Measured against the wrong one it says either nothing, or nonsense — telling a 24.04 instance to install 26.04.2.2. Hence three outcomes:

    • my branch is listed → compared against its own bounds;
    • my branch is above every listed branch → not affected, the fix landed before my line existed;
    • my branch is below the highest listed one and is not itself listed → branch not listed. It means either "this branch was never affected" or "this branch is end-of-life and will never get this fix", and nothing in the data can tell them apart. Reported as information, never as a verdict — but reported, because it is exactly the case where being up to date on your branch still leaves you exposed, and no other signal in this tool can say so.

    Two limitations worth knowing. Enterprise entries are skipped: their version numbers exist nowhere else (31.0.14.5, 30.0.17.9) and nothing here can tell a community instance from an enterprise one, so an enterprise instance on a branch the community no longer maintains is under-reported. And advisories whose bounds are prose only are not evaluated — they are counted once for the fleet rather than attributed to every server, and they are never counted as clean.

    Authentication is optional. GITHUB_TOKEN and NVD_API_KEY are read from the environment (never from config.php — they are credentials). One daily run fits inside the anonymous quotas, so their absence is logged as information, not as a warning. To use one anyway:

    root@kitploit:~
    30 4 * * * cd /var/www/ncstatuscheck && GITHUB_TOKEN=<your-token> php cron-advisories.php
    

    A failing source never empties the cache. It keeps the advisories already collected for it and its entry in sources[] becomes status: "error" with the reason and the date of the last successful fetch; the other sources still refresh. "We learned nothing" and "there is nothing" are different facts, and only one of them is safe to act on. The file is written atomically (temp file + rename()), so a reader never sees a half-written cache.

    Version data is qualified, never guessed. Upstream states affected versions in machine-readable bounds sometimes and in prose the rest of the time, so every advisory carries a version_data verdict:

    ValueMeaning
    structuredaffected is the whole picture — safe to compare a version against.
    partialSome (package, ecosystem) groups were readable, others were not; the unreadable ones are named in unparsed. An absence of match here does not mean an absence of exposure.
    text_onlyVersions are stated in prose only. raw_affected_text keeps it for a human; nothing is parsed out of it.
    noneThe advisory carries no version information at all.

    No version is ever inferred from free text: a false negative would make the dashboard assert "no vulnerability" when there is one, which is worse than no information at all.

    Retention: at most 200 advisories per product, newest first, to cap the file size (~240 KB for the current 142).

    Where it shows in the interface

    A 🛡️ Vulnerabilities synthesis card counts what there is to fix (a CVE whose fix exists for this branch) and to check (a branch the advisory does not list); clicking it opens the list per server. On a server's detail page, a card lists that instance's CVEs right under the version card.

    The Health-column badge is conditional, and that is the whole point. On a real fleet, 7 servers out of 18 are hit by the same Nextcloud CVE and their version pill is already red — a badge there would repeat, one column to the left, what the row already says. So 🛡️ only appears for what nothing else can say: a dead branch (green version pill, and yet no fix will ever come), a Collabora/OnlyOffice CVE (never covered by the version column), or a Nextcloud CVE during the update grace delay. The rule lives in advisories_badge_needed() (PHP), not in the rendering, so the detail page reaches the same verdict.

    Nothing here ever fetches: the pages read cache/advisories.json, which only cron-advisories.php writes. With collection disabled the cache is absent, the card does not appear at all, and the rest of the dashboard is unchanged.

    Docker (alternative to bare-metal)

    Instead of steps 1–6 above, NcStatusCheck can also run as a small docker compose stack (PHP-FPM + nginx + a cron container) — the repo is bind-mounted as-is, no build step or Composer, so it mirrors the bare-metal layout exactly, just containerised. Serves plain HTTP only (port 8080 by default) — put your own TLS-terminating reverse proxy in front of it.

    Full setup — configuration, the permissions gotchas (uid 82, servers.json pre-creation), HTTP Basic Auth, cron, updates and backups — lives entirely in deploy/docker/README.md. Start there; this section is intentionally just a pointer, to avoid keeping two copies of the same steps in sync.

    📋 Usage

    Main interface

    • Go to https://monitoring.your-domain.com
    • View the dashboard with the status of all your servers
    • Use filters to narrow down by status or search
    • Check official versions and the release schedule

    Administration

    • Open the administration interface via the "⚙️ Admin" button
    • A probe modes summary table at the top of the page explains the differences between Basic, Extended, and Push collection methods
    • Server management: each server is displayed as a card with two collapsible probe zones:
      • Serverinfo zone: configure the read token (NC → NcStatusCheck pull)
      • Push zone: generate a push token and download the ready-to-use cron script for the remote instance
    • Nextcloud configuration: set minimum secure versions per branch (hover effects on rows)
    • PHP configuration: set recommended/supported statuses per version

    Detail page

    Accessible by clicking on any server name or its Health indicator.

    For Basic servers (no Extended or Push probe), a simplified page shows the available data (NC version, web server, HTTP protocol) with a notice and a suggestion to enable a probe.

    For Extended / Push servers, the full detail page displays separate sections:

    SectionFields
    Nextcloud systemVersion, debug mode, local/distributed memcache, file locking, disk space
    PHPVersion, memory_limit, upload_max_filesize, max_execution_time, FPM, OPcache
    Web serverName + version, HTTP protocol
    DatabaseType, version, size
    CacheRedis, APCu hit rate
    Active usersLast 5 min, 1 h, 24 h, 7 days

    API

    NcStatusCheck exposes several REST endpoints:

    Main API (api.php)

    • GET ?action=get_data — Fetch data (cache or refresh)
    • POST ?action=refresh_data — Force update of all servers

    Push API (push-api.php)

    • POST with push_token header — Receive push data from a remote NC instance
    • POST ?action=request_push_all — Request an immediate push from all configured push servers (sets a trigger flag consumed by the remote cron script)

    The cron script generated by the admin UI is split in two: a generic core /usr/local/bin/ncstatuscheck-push.sh — identical on every server (all the logic) — driven by a small per-instance config /etc/ncstatuscheck/<slug>.conf (SERVER_URL, SLUG, OCC_CMD, DOCKER_ENABLED, SKOPEO_ENABLED). It is invoked as ncstatuscheck-push.sh /etc/ncstatuscheck/<slug>.conf [--test]. The core refuses to source a group/world-writable config (anti code-injection).

    It is multi-target (fan-out): data is collected once and pushed to every monitor listed in /etc/ncstatuscheck/targets-<slug>.conf (one url|push_token[|http_user|http_pass] line per monitor). Each monitor's admin emits an idempotent command to register itself.

    Multiple Nextcloud instances on one host: per-instance paths are suffixed by a <slug> derived from the monitored URL (e.g. latest.ezeo.coop → latest_ezeo_coop): <slug>.conf, /etc/cron.d/ncstatuscheck-<slug>, targets-<slug>.conf, ncstatuscheck-push-<slug>.log, state …-<slug>.<md5>.last. Only the core is shared, so co-located instances never collide.

    Nextcloud running in Docker (official image, compose, AIO): fully supported — the script is installed on the host (root cron + Docker daemon access), never inside the container, and occ goes through docker exec: OCC_CMD=docker exec -u www-data <container> php occ (AIO container: nextcloud-aio-nextcloud). The admin script generator has an install type preset that prefills this. Never add -t (no TTY under cron); keep -u www-data (the official image refuses occ as root).

    Fleet deployment / updates: because the core is a single identical file, updating the logic across many servers = replacing that one file (the ↑ marker flags servers running an older version). See deploy/ansible/ for a ready-to-use playbook (or a plain scp loop). The monitor stays passive — it never sends code to the fleet; the trust anchor is your own SSH access, not the monitor.

    Migrating from a pre-v4 install (monolithic per-instance script): remove the old /usr/local/bin/ncstatuscheck-push-<slug>.sh and /etc/cron.d/ncstatuscheck-<slug> before installing the core + config (the targets-<slug>.conf is reused as-is), otherwise you double-push.

    Detail API (detail-api.php)

    • GET ?server=<url> — Full serverinfo data + computed warnings for an Extended/Push server

    Administration APIs

    • admin-api.php — Version configuration
    • servers-admin-api.php — Server management (get_servers, add_server, remove_server, update_server_token, generate_push_token, remove_push_token)
    • nextcloud-versions-api.php — Official versions
    • desktop-releases-api.php — Nextcloud desktop client releases (get_desktop_releases, refresh_desktop_releases)

    🩺 Server audit (nc-audit.sh)

    A separate subsystem from monitoring: a standalone, read-only bash script (tools/nc-audit.sh) run as root on a Nextcloud server for a one-off / monthly audit of the web + PHP + database tuning, cross-checked against the machine's physical capacity (RAM, CPU, disk type). Aimed at a managed-supervision offer: the client installs it, the monitor only receives reports — no machine/network access required. The script only reads the configuration (no changes), prints a coloured report and writes a copy to /tmp.

    What it checks: server capacity (RAM/CPU/SSD-HDD, swappiness, shared-server detection) · Nextcloud (versions, cron, cache, Redis runtime, DB type, logs) · PHP/PHP-FPM (real serving SAPI, OPcache runtime, multi-pool memory) · Apache (MPM-aware worker memory) · Nginx · PostgreSQL · MariaDB · security hygiene (fail2ban or CrowdSec + bouncer + community blocklist; pending updates / reboot / services on stale libraries) · RAM budget reconciliation (InnoDB + FPM + Apache vs real RAM) · deep analysis with optional tools if already present (mysqltuner, pt-variable-advisor, apache2buddy, sar/iostat).

    root@kitploit:~
    # Download (the page distributes it; the repo raw URL is public)
    curl -fsSL https://gitlab.com/jp.louvel/ncstatuscheck/-/raw/master/tools/nc-audit.sh -o /usr/local/bin/nc-audit.sh
    chmod 700 /usr/local/bin/nc-audit.sh
    
    sudo nc-audit.sh                                   # auto-detect, dedicated server
    sudo nc-audit.sh /var/www/nextcloud                # explicit path (or NC_PATH=…)
    sudo NC_RAM_BUDGET_PCT=50 nc-audit.sh              # shared host: size to 50% of RAM
    

    Multi-instance hosts (several Nextclouds + a shared database). NC_RAM_BUDGET_PCT is then the total stack budget; NC_PHP_SHARE_PCT% of it (default 60, the rest covers DB + web + OS — lower it on DB-heavy servers) is the PHP share, split across FPM pools by weight (a relative importance — not a percentage, not MB) to give a target pm.max_children per pool:

    root@kitploit:~
    target = PHP_share × (weight / Σ weights) / ~50 MB per process
    

    The target is a ceiling the budget allows, not a value you must set (only raise a pool that actually saturates). Weights are your call — the tool never guesses them.

    root@kitploit:~
    # Weights you provide (a human judgment — the tool never guesses them):
    sudo NC_RAM_BUDGET_PCT=70 NC_INSTANCES="poolA:4,poolB:2,poolC:1" nc-audit.sh
    
    # Interactive helper (terminal only): lists the pools, asks a weight for each,
    # prints the targets + a reusable NC_INSTANCES line:
    sudo NC_RAM_BUDGET_PCT=70 nc-audit.sh --tune-fpm
    

    Report push-back (optional, reuses the Push infrastructure): nc-audit.sh --push /etc/ncstatuscheck/<slug>.conf runs the audit and POSTs the report to the monitor(s), which store it and show it on the server's detail page ("🩺 Server audit" section). Typically a monthly cron. The web page (admin, beta) at audit.php distributes the script (download + inline + GitLab one-liner) and shows its version.

    Deep-analysis tools are never installed by the script — they only run if already present (no curl | bash, no auto-install), each bounded by timeout.

    🔧 Advanced configuration

    Version rule customization

    Evaluation rules are configurable via the administration interface:

    Nextcloud statuses:

    • dev — Development version
    • stable — Current stable version
    • oldstable — Previous supported stable version
    • deprecated — Deprecated version

    PHP statuses:

    • recommended — Recommended version
    • supported — Supported version
    • deprecated — Deprecated version

    Configuration variables

    Edit config.php to adapt the configuration:

    root@kitploit:~
    // Environment: 'dev' or 'prod'
    define('ENV', 'prod');
    
    // Paths and URLs
    define('MONITOR_PATH', '/var/www/ncstatuscheck');
    define('MONITOR_URL', 'https://monitoring.your-domain.com');
    
    // Main server cache duration
    define('CACHE_MAX_AGE', 86400); // 24 hours
    
    // Official Nextcloud versions cache duration
    define('VERSIONS_CACHE_AGE', 86400);
    
    // Consecutive failed probes before a server is marked "down" (min 1)
    define('UPTIME_FAIL_THRESHOLD', 2);
    
    // Proactive alerts — webhook on a confirmed up/down state change.
    // Empty URL = disabled. Format: 'slack' (default, also Mattermost/Google Chat),
    // 'discord', or 'raw' (structured JSON). The URL usually carries a secret, so it
    // is never logged in full — see config-example.php for details.
    define('ALERT_WEBHOOK_URL', '');
    define('ALERT_WEBHOOK_FORMAT', 'slack');
    
    // Check alerts on top of up/down (cron-update cadence, 2×/day): SSL expiry
    // tiers, vulnerable (below min_secure) or deprecated Nextcloud version, stale
    // Push data, critical audit report, blocking apps-audit finding. Edge-triggered with a persisted state
    // (cache/alerts_state.json): one alert per NEW condition, no reminders, re-arms
    // when resolved (renewed cert, fixed/acked app…). First run arms silently.
    define('ALERT_CHECKS', 'ssl,version,push_stale,audit,apps'); // '' = up/down only
    define('ALERT_SSL_DAYS', '30,14,7');                          // days-left tiers
    
    // Email channel, independent of the webhook (either one arms the alerting).
    // One digest mail per batch. Recommended transport: direct SMTP submission to
    // your mail server (vendored PHPMailer, lib/phpmailer/ — nothing to set up on
    // the host). Without ALERT_SMTP_HOST it falls back to PHP mail() (local MTA).
    define('ALERT_EMAIL_TO', '');    // comma list of recipients, '' = off
    define('ALERT_EMAIL_FROM', '');  // default: ncstatuscheck@<hostname>
    define('ALERT_SMTP_HOST', '');   // e.g. 'mail.example.org', '' = mail() fallback
    define('ALERT_SMTP_PORT', 587);
    define('ALERT_SMTP_SECURITY', 'starttls'); // 'starttls' | 'tls' | 'none'
    define('ALERT_SMTP_USER', '');
    define('ALERT_SMTP_PASS', '');
    

    See config-example.php for the full, commented list of options (including DEMO_MODE and PUSH_SCRIPT_VERSION).

    🛡️ Security

    Implemented measures

    • Mandatory HTTP Basic Authentication, doubled by an application-level auth check on every admin endpoint (defense in depth)
    • CSRF protection: HMAC token automatically injected on every mutating request (lib/csrf-client.js + csrf_require())
    • Anti-SSRF guard on every operator-supplied URL (loopback, RFC1918, link-local rejected; HTTPS-only redirects; post-connect IP check)
    • HTTPS required with Let's Encrypt certificates
    • Protection of data files and logs: nginx rules (see above) mirrored by shipped .htaccess files for Apache; servers.json chmod'ed 0600 automatically (tokens inside)
    • Security headers (CSP, X-Frame-Options, nosniff, Referrer-Policy) on every PHP-served page
    • Strict URL validation on all inputs
    • Root-run client scripts: the instance config is sourced as root and the targets file decides where collected data is sent — both are refused unless they are neither group/world-writable nor owned by a third party
    • Isolation between monitored instances: a push is only accepted when the URL in the body matches an entry AND hash_equals() passes on that entry's token, so a compromised monitored server can neither read nor overwrite another one's data. request_push / request_push_all sit behind admin auth + CSRF. Verified end to end against a compromised-client scenario
    • Push target reporting: each instance declares which monitors it pushes to (URLs only, never tokens); the detail page shows them and an alert fires when the set changes — an extra line in targets.conf silently copies every push to a third party otherwise
    • Push payload sanitization: app ids are character-allowlisted; free-form Docker fields (name, image, version, status) are stripped of <>"'& at ingestion, on top of escaping at render time
    • SSL/TLS verification for outgoing connections — never disabled, including the SMTP alert transport
    • Bounded remote responses (2 MB): a monitored server fully controls what it answers and nothing authenticates that direction. Unbounded, a server that just streams data exhausts PHP's memory — a fatal error no try/catch can catch, which would kill the collection run mid-loop and, with it, every alert for the whole fleet
    • Demo-mode anonymisation covers more than the URL: container names, private registry hosts in image references and the hostname quoted inside cURL error messages are all scrubbed, on both the dashboard and the detail page

    Recommendations

    • Use strong passwords for HTTP authentication
    • Restrict access to known addresses — the admin IP filtering tab generates the rules for you (see below)
    • Monitor logs for intrusion attempts
    • Keep PHP and system dependencies up to date

    config.php health banner (admin)

    config.php is gitignored and hand-edited per server, so it drifts — silently, since nearly every constant has a fallback in the code. A banner at the top of the admin page reports what is actually wrong, and only when something is: a PUSH_SCRIPT_VERSION left behind by a bump, no alert transport configured at all, a trailing closing tag emitting a byte before any header(), an unwritable cache directory, constants absent and silently falling back to defaults.

    Read-only by design and with no save action, for the same reason as the Notifications tab: config.php is root-owned and holds secrets. The file content never travels — only facts about it — and no secret is read.

    IP filtering (admin tab)

    Everything above is application-level: anyone on the internet can still reach the monitor and probe it, and only the password stops them. The IP filtering tab generates the rules that put an allowlist in front of the app, so unknown hosts cannot talk to it at all. It is defense in depth, not a replacement for Basic auth or the push tokens — and it only ever produces text to review and paste, it never writes a web server or firewall config.

    Two classes of source, deliberately unequal, so a compromised monitored server cannot reach the admin:

    ClassWhoMay reach
    pushmonitored instances in Push mode only/push-api.php, nothing else
    adminbastion / VPN / fixed office IPeverything

    Instances polled in Basic/Extended mode open no inbound connection and get no allowlist entry at all.

    Addresses come from two sources, and the difference matters: a monitored domain's DNS record is its ingress address, while its push leaves from its egress. Where they differ, only the second one works. push-api.php therefore records the real source address of every push (source_ip in the push cache), and the tab allowlists that, reporting the mismatch. Until a server has pushed once, it falls back to DNS A+AAAA and says so.

    Three outputs:

    • nginx (recommended) — a self-contained conf.d file (geo + map) plus a single if ($ncsc_forbidden) { return 403; } line in the vhost. No need to duplicate the fastcgi block, static files are covered too (admin.html is one), and /.well-known/acme-challenge/ stays open so certificate renewal cannot silently break.
    • Apache 2.4 — <LocationMatch> with a negative lookahead plus a <Location> for the push endpoint, so the two sections cannot overlap and nothing depends on Apache's merge order. All addresses of a rule go on one Require ip line: several lines inside <RequireAll> are ANDed, which nobody can satisfy.
    • ufw — coarse by nature (a packet filter sees ports, not URLs), so it cannot express the push/admin split. Use it as an outer layer only.

    The generator refuses to emit anything when no administration address is given, warns when the operator's own address is not covered, and warns when the request came through a proxy (both geo and Require ip read the transport peer, so behind a proxy every client looks alike). The generated ufw snippet puts the SSH rule first, keeps port 80 open for the HTTP-01 challenge, and spells out the IPv6 trap: unlike nginx, which refuses an unlisted v6 address, ufw does not filter v6 at all unless IPV6=yes is set — a dual-stack host would otherwise be wide open over IPv6.

    Known limit, surfaced in the page itself: once the rules are applied, this tab discovers nothing new. A refused push is rejected by the web server before it reaches PHP, so the recorded address stays the last one that got through — and still looks verified. Two consequences: adding a Push server means regenerating and reapplying the rules, or its first push is refused; and if an instance's address changes, the new one is only readable in the web server access log (grep 'push-api.php' access.log | grep ' 403 '). The tab therefore shows the last-seen date of every observed address and flags it once it is older than a full missed push cycle — the same threshold as the push_stale alert, which covers the same blind spot from the other side.

    Telling a filtering problem from any other one: a bare GET on the push endpoint separates the layers cleanly, with no side effect and no token needed — run it from the machine concerned, since what is judged is that machine's outgoing address:

    root@kitploit:~
    curl -sS -o /dev/null -w '%{http_code}\n' https://your-monitor/push-api.php
    
    AnswerMeaning
    403blocked by the IP filtering
    401filtering passed, Basic auth is answering — the problem is elsewhere
    405the request reached the application (GET is not an accepted method there)
    nothing / timeoutnot the filtering: a filter answers, it does not go silent

    Re-run with -u user:password to settle a doubt on a 403: if the code does not move, it really is the filtering. Verified on both nginx and Apache (including with Require valid-user enabled), the filter answers before authentication — and a 403 coming from the application itself always carries JSON in the body.

    The snippet logic lives in lib/hardening-rules.php, which is pure and covered by tests/run.php: the snippets are the product here, and a wrong one either locks the operator out or leaves a hole. Both the nginx and the Apache output have been verified behaviourally (real servers, real source addresses, including path-traversal attempts from the push class).

    Automated analysis (CI security stage)

    Dependency scanning (npm/pnpm audit, Snyk Open Source, Dependabot) is a no-op here: there is no package.json and no composer.json — nothing to scan. The risk lives in the custom code (~15k lines of PHP, ~6k of JS) and in the shell scripts that run as root on monitored instances (tools/*.sh). The pipeline is aimed there:

    JobToolBlockingScope
    secrets_scangitleaksyescommitted secrets (working tree)
    sast_semgrepsemgrep (p/php, p/javascript, p/owasp-top-ten)yesSSRF, missing authz/CSRF, XSS
    shellcheckshellcheck (--severity=warning)yestools/*.sh — root on client hosts
    dockerfile_misconfigtrivy misconfigyesdeploy/docker/
    container_cvetrivy imageno (allow_failure)the image deploy/docker builds, plus the web image compose runs
    ui_testsnode (no deps)yesescaping invariants of lib/ui-common.js (both past XSS regressions)
    phpmailer_freshnessGitHub APIno (allow_failure)vendored pin vs upstream release
    deploy_selfchecknc-selfcheck.shyesthe shipped nginx ruleset (deny rules + security headers) stood up in a throwaway container

    All blocking jobs have a zero-finding baseline, so any new alert is a real signal. Two deliberate calls, documented inline in .gitlab-ci.yml:

    • php.lang.security.injection.echoed-request is excluded from semgrep: it flags every echo json_encode() as XSS, which is what every API endpoint here legitimately does (JSON responses, not HTML). It accounted for 10 out of 10 findings on the first run, all false. Keeping it would train everyone to ignore the job.
    • The two allow_failure jobs report upstream facts (a CVE in a base image or one whose fix has not reached the Alpine branch yet, a new PHPMailer release) that a merge request usually cannot fix. Red-but-tolerated is the accurate signal — "time to rebuild / refresh the vendoring" — not a reason to block unrelated work. phpmailer_freshness reports an unreachable or rate-limited GitHub API as a skip, never as "outdated".
    • container_cve scans the image it builds, not the FROM tag. The Dockerfile hardens the base with apk --no-cache upgrade (the official PHP image lags behind the Alpine repos — it shipped c-ares 1.34.6-r0 while 1.34.8-r0, fixing CVE-2026-33630, was already published). Scanning the base tag would therefore report CVEs the shipped image no longer has: a permanently orange job nobody reads.

    Allowlists are intentionally narrow: .gitleaks.toml excuses literal placeholder strings, never whole documentation files (allowlisting README.md would blind the scan the day a real secret is pasted into it) — so a new example token in the docs must be added there. .trivyignore holds a single entry, DS-0002, argued in the file: the php-fpm master must start as root to drop its workers to www-data (uid 82).

    Post-deployment verification (nc-selfcheck.sh)

    CI can lock the shipped config (the deploy_selfcheck job above stands the nginx ruleset up in a container and probes it), but it cannot verify the server you actually deployed to — different host, Basic-auth credentials, filesystem permissions. tools/nc-selfcheck.sh closes that gap. It is a standalone, read-only bash script (same model as nc-audit.sh) you run after each deploy:

    root@kitploit:~
    # Black-box, no credentials: confirms Basic auth is enforced (401) and that
    # sensitive files are blocked (cache/, servers.*, .git, config.php source).
    bash tools/nc-selfcheck.sh https://monitoring.example.com
    
    # + security headers behind Basic auth:
    bash tools/nc-selfcheck.sh -u user:pass https://monitoring.example.com
    
    # + filesystem checks (run ON the host): servers.json / config.php / CSRF-secret
    # permissions, and a stray closing "?>" in config.php.
    bash tools/nc-selfcheck.sh --webroot /var/www/ncstatuscheck https://monitoring.example.com
    

    It exits non-zero on any critical finding (source leak, unblocked secret file, world-readable token store, missing Basic auth), so it can gate a rollout — wire it into your sync/deploy script as a post-step. WARN/INFO never fail the run.

    🧪 Testing and development

    Development mode

    root@kitploit:~
    // In config.php
    define('ENV', 'dev');
    

    In development mode, additional information is displayed (PHP version, web server).

    Server testing

    Use the administration interface to add a server by URL. The server will be polled on the next data refresh.

    Debug logs

    Check the log files in cache/:

    • monitor.log — General application logs
    • cron.log — Full collection script logs (cron-update.php)
    • ping.log — Lightweight up/down probe logs (cron-ping.php)
    • advisories.log — Security advisory collection logs (cron-advisories.php)
    • alerts.log — Proactive alert dispatch (webhook/email), never logs webhook secrets or SMTP credentials

    Test suite

    root@kitploit:~
    php tests/run.php   # plain-PHP assertions, no framework — exit 0 = all green
    

    Covers the pure business logic (version/apps rules, warnings, uptime state machine and availability, alert dedup/re-arm state machine, email builders).

    🤝 Contributing

    Reporting a bug

    Open a new issue with:

    • A detailed description of the problem
    • Steps to reproduce
    • Error logs if available
    • Your environment configuration

    Proposing an improvement

    1. Open an issue to discuss your idea
    2. Fork the project
    3. Create a branch for your feature
    4. Implement with documentation
    5. Submit a merge request

    📝 License

    This project is licensed under GNU AGPL v3.

    👥 Credits

    NcStatusCheck is developed by ézéo, a digital cooperative specializing in open source solutions.

    Contributors

    • ézéo team — Initial development and maintenance

    Need help? Check the issues or contact the ézéo team.