From 2919722daba67ed33f1be120d6bebf727e9ace6b Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Thu, 4 Jun 2026 06:44:14 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20Part=201=20batch=20=E2=80=94=20security?= =?UTF-8?q?=20headers,=20auto-ban,=20SSL=20monitor,=20IP=20display,=20sett?= =?UTF-8?q?ings=20export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #118 Display current IP on dashboard info bar #124 HTTP security headers at runtime (X-Frame, X-Content-Type, X-XSS, Referrer-Policy, HSTS, CSP, Permissions-Policy) — new firewall fieldset with per-header toggles #143 WAF auto-ban: threshold + window params, auto-adds to IP blocklist after N blocks in M minutes #148 SSL certificate expiry monitoring in cpanel module (green/yellow/red badge with days remaining) #132 Settings import/export — export all plugin + component params as JSON, import on another site Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/Controller/DisplayController.php | 98 +++++++++++++ .../admin/tmpl/dashboard/default.php | 4 + .../src/Dispatcher/Dispatcher.php | 1 + .../src/Helper/CpanelHelper.php | 50 +++++++ .../mod_mokowaas_cpanel/tmpl/default.php | 6 + .../mokowaas_firewall.xml | 57 ++++++++ .../src/Extension/Firewall.php | 132 +++++++++++++++++- 7 files changed, 347 insertions(+), 1 deletion(-) diff --git a/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php b/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php index e7696638..e910b66c 100644 --- a/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php +++ b/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php @@ -270,6 +270,104 @@ class DisplayController extends BaseController } } + // ================================================================== + // Settings Import/Export (#132) + // ================================================================== + + public function exportSettings() + { + if (!$this->checkAcl('core.admin')) + { + $this->jsonForbidden(); + } + + $db = Factory::getDbo(); + $settings = []; + + // Export all MokoWaaS plugin params + $plugins = ['mokowaas', 'mokowaas_firewall', 'mokowaas_tenant', 'mokowaas_devtools', 'mokowaas_offline']; + + foreach ($plugins as $element) + { + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote($element)) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + ); + $settings['plugins'][$element] = json_decode($db->loadResult() ?? '{}', true); + } + + // Export component params + $db->setQuery( + $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + ); + $settings['component'] = json_decode($db->loadResult() ?? '{}', true); + $settings['exported'] = gmdate('Y-m-d\TH:i:s\Z'); + $settings['site'] = Factory::getConfig()->get('sitename', ''); + + $this->jsonResponse(['success' => true, 'settings' => $settings]); + } + + public function importSettings() + { + Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + + if (!$this->checkAcl('core.admin')) + { + $this->jsonForbidden(); + } + + $json = Factory::getApplication()->getInput()->getRaw('settings_json', ''); + $data = json_decode($json, true); + + if (empty($data) || empty($data['plugins'])) + { + $this->jsonResponse(['success' => false, 'message' => 'Invalid settings JSON.']); + } + + $db = Factory::getDbo(); + $count = 0; + + foreach ($data['plugins'] ?? [] as $element => $params) + { + if (!is_array($params)) + { + continue; + } + + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($params))) + ->where($db->quoteName('element') . ' = ' . $db->quote($element)) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + )->execute(); + $count++; + } + + if (!empty($data['component']) && is_array($data['component'])) + { + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote(json_encode($data['component']))) + ->where($db->quoteName('element') . ' = ' . $db->quote('com_mokowaas')) + ->where($db->quoteName('type') . ' = ' . $db->quote('component')) + )->execute(); + $count++; + } + + $this->jsonResponse(['success' => true, 'message' => "Imported settings for {$count} extensions."]); + } + // ================================================================== // WAF Log // ================================================================== diff --git a/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php b/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php index 4da50048..186ad4f0 100644 --- a/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php +++ b/src/packages/com_mokowaas/admin/tmpl/dashboard/default.php @@ -65,6 +65,10 @@ $categoryOrder = ['core', 'security', 'monitoring', 'content', 'tools', 'api']; offline): ?> +
+ + escape($_SERVER['REMOTE_ADDR'] ?? ''); ?> +
diff --git a/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php b/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php index d3b1c191..d5b14142 100644 --- a/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php +++ b/src/packages/mod_mokowaas_cpanel/src/Dispatcher/Dispatcher.php @@ -33,6 +33,7 @@ class Dispatcher extends AbstractModuleDispatcher implements HelperFactoryAwareI $data['counts'] = $helper->getCounts($db); $data['disk'] = $helper->getDiskInfo(); $data['currentIp'] = $helper->getCurrentIp(); + $data['ssl'] = $helper->getSslStatus(); return $data; } diff --git a/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php b/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php index 7160329e..a50d895e 100644 --- a/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php +++ b/src/packages/mod_mokowaas_cpanel/src/Helper/CpanelHelper.php @@ -136,4 +136,54 @@ class CpanelHelper { return $_SERVER['REMOTE_ADDR'] ?? ''; } + + /** + * Check SSL certificate expiry (#148). + * + * @return object|null {expires, days_remaining, warning} or null if check fails + */ + public function getSslStatus(): ?object + { + try + { + $host = parse_url(\Joomla\CMS\Uri\Uri::root(), PHP_URL_HOST); + + if (empty($host)) + { + return null; + } + + $context = stream_context_create(['ssl' => ['capture_peer_cert' => true, 'verify_peer' => false]]); + $client = @stream_socket_client('ssl://' . $host . ':443', $errno, $errstr, 5, STREAM_CLIENT_CONNECT, $context); + + if (!$client) + { + return null; + } + + $params = stream_context_get_params($client); + fclose($client); + + $cert = openssl_x509_parse($params['options']['ssl']['peer_certificate'] ?? ''); + + if (empty($cert['validTo_time_t'])) + { + return null; + } + + $expires = $cert['validTo_time_t']; + $days = (int) floor(($expires - time()) / 86400); + + return (object) [ + 'expires' => date('Y-m-d', $expires), + 'days_remaining' => $days, + 'warning' => $days <= 30, + 'critical' => $days <= 7, + ]; + } + catch (\Throwable $e) + { + return null; + } + } } diff --git a/src/packages/mod_mokowaas_cpanel/tmpl/default.php b/src/packages/mod_mokowaas_cpanel/tmpl/default.php index b3ed62f2..a65553f5 100644 --- a/src/packages/mod_mokowaas_cpanel/tmpl/default.php +++ b/src/packages/mod_mokowaas_cpanel/tmpl/default.php @@ -130,6 +130,12 @@ $diskColor = ($diskPct !== null && $diskPct > 90) ? 'bg-danger' : (($diskPct !== + + + + SSL days_remaining; ?>d + + Jjoomla_version ?? ''); ?> / PHP php_version ?? ''); ?> diff --git a/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml b/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml index f2b25dfb..b67360d2 100644 --- a/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml +++ b/src/packages/plg_system_mokowaas_firewall/mokowaas_firewall.xml @@ -127,6 +127,53 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + checkDirectPhpAccess(); } - // Existing features + // Security headers + existing features + $this->injectSecurityHeaders(); $this->enforceHttps(); $this->enforceUploadRestrictions(); @@ -400,6 +401,28 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi 'created' => gmdate('Y-m-d H:i:s'), ]; $db->insertObject('#__mokowaas_waf_log', $row); + + // Auto-ban: if IP has N+ blocks in last M minutes, add to blocklist (#143) + $threshold = (int) $this->params->get('autoban_threshold', 10); + $window = (int) $this->params->get('autoban_window', 5); + + if ($threshold > 0 && $window > 0) + { + $cutoff = gmdate('Y-m-d H:i:s', time() - ($window * 60)); + $db->setQuery( + $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokowaas_waf_log')) + ->where($db->quoteName('ip') . ' = ' . $db->quote($ip)) + ->where($db->quoteName('created') . ' >= ' . $db->quote($cutoff)) + ); + $recentBlocks = (int) $db->loadResult(); + + if ($recentBlocks >= $threshold) + { + $this->autoBanIp($ip, $db); + } + } } catch (\Throwable $e) { @@ -418,6 +441,51 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi // Input Scanning // ================================================================== + /** + * Auto-ban an IP by adding it to the blocklist params (#143). + */ + private function autoBanIp(string $ip, $db): void + { + try + { + $query = $db->getQuery(true) + ->select($db->quoteName('params')) + ->from($db->quoteName('#__extensions')) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')); + $db->setQuery($query); + $params = new \Joomla\Registry\Registry($db->loadResult() ?? '{}'); + $blocklist = json_decode($params->get('ip_blocklist', '[]'), true) ?: []; + + foreach ($blocklist as $entry) + { + if (($entry['ip'] ?? '') === $ip) + { + return; + } + } + + $blocklist[] = ['ip' => $ip, 'enabled' => '1', 'label' => 'Auto-banned by WAF (' . gmdate('Y-m-d H:i') . ')']; + $params->set('ip_blocklist', json_encode($blocklist)); + + $db->setQuery( + $db->getQuery(true) + ->update($db->quoteName('#__extensions')) + ->set($db->quoteName('params') . ' = ' . $db->quote($params->toString())) + ->where($db->quoteName('element') . ' = ' . $db->quote('mokowaas_firewall')) + ->where($db->quoteName('type') . ' = ' . $db->quote('plugin')) + ->where($db->quoteName('folder') . ' = ' . $db->quote('system')) + )->execute(); + + Log::add('WAF auto-banned IP: ' . $ip, Log::WARNING, 'mokowaas'); + } + catch (\Throwable $e) + { + // Silent + } + } + private function scanInput(array $input, string $pattern): ?string { foreach ($input as $key => $value) @@ -547,6 +615,68 @@ class Firewall extends CMSPlugin implements SubscriberInterface, BootableExtensi } } + /** + * Inject HTTP security headers at runtime (#124). + */ + private function injectSecurityHeaders(): void + { + $app = $this->getApplication(); + + if ($app->isClient('cli')) + { + return; + } + + if ($this->params->get('header_xframe', 1)) + { + $app->setHeader('X-Frame-Options', 'SAMEORIGIN', true); + } + + if ($this->params->get('header_xcontent', 1)) + { + $app->setHeader('X-Content-Type-Options', 'nosniff', true); + } + + if ($this->params->get('header_xxss', 1)) + { + $app->setHeader('X-XSS-Protection', '1; mode=block', true); + } + + $referrer = $this->params->get('header_referrer', ''); + + if (!empty($referrer) && $referrer !== 'off') + { + $app->setHeader('Referrer-Policy', $referrer, true); + } + + if ($this->params->get('header_hsts', 0)) + { + $maxAge = (int) $this->params->get('header_hsts_maxage', 31536000); + $hsts = 'max-age=' . $maxAge; + + if ($this->params->get('header_hsts_subdomains', 0)) + { + $hsts .= '; includeSubDomains'; + } + + $app->setHeader('Strict-Transport-Security', $hsts, true); + } + + $csp = $this->params->get('header_csp', ''); + + if (!empty($csp)) + { + $app->setHeader('Content-Security-Policy', $csp, true); + } + + $perms = $this->params->get('header_permissions', ''); + + if (!empty($perms)) + { + $app->setHeader('Permissions-Policy', $perms, true); + } + } + private function enforceHttps(): void { if (!$this->params->get('force_https', 0))