Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ secure:
runs on (PHP, the web server, the database, Redis …) marked
- whether a reboot is needed, and why (a newer kernel, updated core libraries,
the distribution asking for one), with live kernel patches taken into account
- whether the PHP serving Nextcloud still runs libraries that an update
replaced, and which service to restart
- how long the release still receives security updates, and whether a paid
extension (Ubuntu Pro or Debian ELTS) is active
- automatic updates, package list age, held packages, failed system services,
Expand All @@ -39,7 +41,7 @@ what can and cannot be known from inside; LXC and systemd-nspawn guests are
treated like machines without a kernel of their own.

Nothing is installed or changed. To find out, the app reads `/etc/os-release`,
`/proc` (uptime, `/proc/1/cgroup`), `/sys/class/dmi/id`,
`/proc` (uptime, `/proc/1/cgroup`, its own process maps), `/sys/class/dmi/id`,
`/sys/kernel/livepatch`, the SELinux and AppArmor state under `/sys`, the
package manager's configuration, state and logs, `/run/reboot-required` and the
installed kernel modules. It runs the package manager's query commands (updates
Expand Down Expand Up @@ -69,7 +71,9 @@ occ serverinfo:os-status [--refresh] [--output=json]
The command shows how old the status is and exits with 0, 1, 2 or 3 for ok,
warning, error and unknown, so it can be used as a monitoring check. Unknown means the status has not been
collected yet, or nothing is wrong but the pending updates could not be
determined. When something counts as a warning or an error is set by
determined. The same verdict appears as setup checks on Settings → Overview, in
a notification to admins when a concern escalates, and in the **Server health**
dashboard widget. When something counts as a warning or an error is set by
the `os_*_days` app config values, see `lib/Config/ConfigLexicon.php`.

Release support dates are bundled with the app (see
Expand Down Expand Up @@ -131,6 +135,35 @@ https://<nextcloud-fqdn>/ocs/v2.php/apps/serverinfo/api/v1/info
- To request the information in JSON append the url parameter `format=json`
- Use the url parameter `skipUpdate=true` to omit server updates.
- Use the url parameter `skipApps=true` to omit app updates (including available app updates will send an external request to the app store).
- Use the url parameter `skipOs=false` to include the operating system status.

The operating system status in `server.os` holds counts and flags only, never
package names, package or kernel versions, or advisories, so a leaked token
does not reveal what is unpatched. Values that could not be determined are
`null`:

```json
"os": {
"state": "ok",
"level": "warning",
"collectedAt": 1792411200,
"outdated": false,
"name": "Ubuntu 24.04.3 LTS",
"environment": "host",
"container": null,
"updates": { "classified": true, "security": 7, "total": 23, "pendingDays": 5, "held": 0 },
"rebootRequired": true,
"rebootPendingDays": 2,
"phpRestartRequired": false,
"servicesNeedRestart": null,
"support": { "state": "supported", "ends": "2029-05-31", "daysLeft": 954 },
"automaticUpdates": "enabled",
"listsAgeHours": 4,
"timeSynchronized": true,
"failedUnits": 0,
"helper": "absent"
}
```

### Example XML output:
```
Expand Down
5 changes: 5 additions & 0 deletions img/status-error.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions img/status-info.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions img/status-ok.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions img/status-warning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@
namespace OCA\ServerInfo\AppInfo;

use OCA\ServerInfo\Config\ConfigLexicon;
use OCA\ServerInfo\Dashboard\ServerHealthWidget;
use OCA\ServerInfo\OsStatus\Host\IHost;
use OCA\ServerInfo\OsStatus\Host\LocalHost;
use OCA\ServerInfo\OsStatus\Notifications\OsNotifier;
use OCA\ServerInfo\SetupChecks\DatabaseChecks;
use OCA\ServerInfo\SetupChecks\OsRebootCheck;
use OCA\ServerInfo\SetupChecks\OsSupportCheck;
use OCA\ServerInfo\SetupChecks\OsUpdatesCheck;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
Expand All @@ -32,7 +37,12 @@ public function __construct() {
public function register(IRegistrationContext $context): void {
$context->registerConfigLexicon(ConfigLexicon::class);
$context->registerSetupCheck(DatabaseChecks::class);
$context->registerSetupCheck(OsUpdatesCheck::class);
$context->registerSetupCheck(OsRebootCheck::class);
$context->registerSetupCheck(OsSupportCheck::class);
$context->registerServiceAlias(IHost::class, LocalHost::class);
$context->registerNotifierService(OsNotifier::class);
$context->registerDashboardWidget(ServerHealthWidget::class);
}

#[\Override]
Expand Down
3 changes: 3 additions & 0 deletions lib/Commands/ShowOsStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace OCA\ServerInfo\Commands;

use OC\Core\Command\Base;
use OCA\ServerInfo\OsStatus\Notifications\OsNotificationSender;
use OCA\ServerInfo\OsStatus\OsStatusCollector;
use OCA\ServerInfo\OsStatus\OsStatusService;
use OCA\ServerInfo\OsStatus\Verdict\Concern;
Expand All @@ -30,6 +31,7 @@ class ShowOsStatus extends Base {
public function __construct(
private OsStatusCollector $collector,
private OsStatusService $service,
private OsNotificationSender $notifications,
) {
parent::__construct();
}
Expand All @@ -49,6 +51,7 @@ public function execute(InputInterface $input, OutputInterface $output): int {

return self::EXIT_UNKNOWN;
}
$this->notifications->sync();
}

$status = $this->service->getStatus();
Expand Down
24 changes: 24 additions & 0 deletions lib/Config/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,11 @@ class ConfigLexicon implements ILexicon {
public const CACHED_OS_FIRST_SEEN = 'cached_os_first_seen';
public const CACHED_OS_PHP_PROCESS = 'cached_os_php_process';
public const CACHED_OS_PHP_OUTDATED_SINCE = 'cached_os_php_outdated_since';
public const OS_NOTIFIED_LEVELS = 'os_notified_levels';
public const OS_NOTIFIED_USERS = 'os_notified_users';
public const JOB_INTERVAL_OS_STATUS = 'job_interval_os_status';
public const OS_HELPER_REPORT_PATH = 'os_helper_report_path';
public const OS_NOTIFICATIONS = 'os_notifications';
public const OS_SECURITY_WARNING_DAYS = 'os_security_warning_days';
public const OS_SECURITY_ERROR_DAYS = 'os_security_error_days';
public const OS_REBOOT_WARNING_DAYS = 'os_reboot_warning_days';
Expand Down Expand Up @@ -107,6 +110,20 @@ public function getAppConfigs(): array {
'When web requests first saw their PHP process running replaced libraries, 0 when they do not',
lazy: true,
),
new Entry(
self::OS_NOTIFIED_LEVELS,
ValueType::ARRAY,
[],
'Alert level each operating system concern was last notified at, so admins are told once per escalation',
lazy: true,
),
new Entry(
self::OS_NOTIFIED_USERS,
ValueType::ARRAY,
[],
'Admins each operating system concern was notified to since it last escalated, so a dismissed notification is not sent again',
lazy: true,
),
new Entry(
self::JOB_INTERVAL_OS_STATUS,
ValueType::INT,
Expand All @@ -120,6 +137,13 @@ public function getAppConfigs(): array {
'Where the optional root helper bin/serverinfo-os-report writes its report; an absolute path ending in .json',
lazy: true,
),
new Entry(
self::OS_NOTIFICATIONS,
ValueType::BOOL,
true,
'Whether admins are notified when an operating system concern escalates',
lazy: true,
),
new Entry(self::OS_SECURITY_WARNING_DAYS, ValueType::INT, 3, 'Days a security update may be pending before a warning', lazy: true),
new Entry(self::OS_SECURITY_ERROR_DAYS, ValueType::INT, 14, 'Days a security update may be pending before an error', lazy: true),
new Entry(self::OS_REBOOT_WARNING_DAYS, ValueType::INT, 7, 'Days a reboot may be pending before a warning', lazy: true),
Expand Down
13 changes: 10 additions & 3 deletions lib/Controller/ApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use OCA\ServerInfo\DatabaseStatistics;
use OCA\ServerInfo\FpmStatistics;
use OCA\ServerInfo\Os;
use OCA\ServerInfo\OsStatus\OsStatusService;
use OCA\ServerInfo\PhpStatistics;
use OCA\ServerInfo\SessionStatistics;
use OCA\ServerInfo\ShareStatistics;
Expand Down Expand Up @@ -42,6 +43,7 @@ public function __construct(
private ShareStatistics $shareStatistics,
private SessionStatistics $sessionStatistics,
private UptimeFormatter $uptimeFormatter,
private OsStatusService $osStatus,
) {
parent::__construct($appName, $request);
}
Expand Down Expand Up @@ -79,14 +81,14 @@ private function checkAuthorized(): bool {
* @PublicPage
* @BruteForceProtection(action=serverinfo)
*/
public function info(bool $skipApps = true, bool $skipUpdate = true): DataResponse {
public function info(bool $skipApps = true, bool $skipUpdate = true, bool $skipOs = true): DataResponse {
if (!$this->checkAuthorized()) {
$response = new DataResponse(['message' => 'Unauthorized']);
$response->throttle();
$response->setStatus(Http::STATUS_UNAUTHORIZED);
return $response;
}
return new DataResponse([
$data = [
'nextcloud' => [
'system' => $this->systemStatistics->getSystemStatistics($skipApps, $skipUpdate),
'storage' => $this->storageStatistics->getStorageStatistics(),
Expand All @@ -99,7 +101,12 @@ public function info(bool $skipApps = true, bool $skipUpdate = true): DataRespon
'database' => $this->databaseStatistics->getDatabaseStatistics()
],
'activeUsers' => $this->sessionStatistics->getSessionStatistics()
]);
];
if (!$skipOs) {
$data['server']['os'] = $this->osStatus->monitoringSummary();
}

return new DataResponse($data);
}

public function BasicData(): DataResponse {
Expand Down
Loading
Loading