From 8f936fc92cc1b43924f6f7640749d455eca80b8f Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Wed, 3 Jun 2026 23:42:18 -0500 Subject: [PATCH] feat: WAF log viewer with filters, one-click ban, and purge (#144) Admin view at MokoWaaS > WAF Log: - Rule distribution cards showing block counts per shield - Filterable log table: rule, IP, search (URI/detail/UA), date range - Pagination (50 per page) - One-click IP ban from any log entry or top IPs sidebar - Top 10 blocked IPs sidebar with ban buttons - Purge old logs (configurable days) - Color-coded rule badges (sqli=red, xss=red, mua=yellow, etc.) WaflogModel: - getLogs with filters + pagination - getTotal for page count - getRuleCounts for distribution cards - getTopIps for sidebar - getRuleNames for filter dropdown - purgeLogs(days) with affected count - banIp adds to firewall plugin IP blocklist params ACL: core.admin (Super Users only) Submenu: MokoWaaS > WAF Log Authored-by: Moko Consulting Co-Authored-By: Claude Opus 4.6 (1M context) --- .../admin/language/en-GB/com_mokowaas.sys.ini | 1 + .../src/Controller/DisplayController.php | 35 +++ .../admin/src/Model/WaflogModel.php | 215 ++++++++++++++++++ .../admin/src/View/Waflog/HtmlView.php | 55 +++++ .../admin/tmpl/waflog/default.php | 212 +++++++++++++++++ src/packages/com_mokowaas/mokowaas.xml | 1 + 6 files changed, 519 insertions(+) create mode 100644 src/packages/com_mokowaas/admin/src/Model/WaflogModel.php create mode 100644 src/packages/com_mokowaas/admin/src/View/Waflog/HtmlView.php create mode 100644 src/packages/com_mokowaas/admin/tmpl/waflog/default.php diff --git a/src/packages/com_mokowaas/admin/language/en-GB/com_mokowaas.sys.ini b/src/packages/com_mokowaas/admin/language/en-GB/com_mokowaas.sys.ini index e5da7db4..0218a06c 100644 --- a/src/packages/com_mokowaas/admin/language/en-GB/com_mokowaas.sys.ini +++ b/src/packages/com_mokowaas/admin/language/en-GB/com_mokowaas.sys.ini @@ -13,4 +13,5 @@ COM_MOKOWAAS_MENU_CHECKIN="Global Check-in" COM_MOKOWAAS_MENU_TICKETS="Helpdesk" COM_MOKOWAAS_MENU_HTACCESS=".htaccess Maker" COM_MOKOWAAS_MENU_PRIVACY="Privacy Guard" +COM_MOKOWAAS_MENU_WAFLOG="WAF Log" COM_MOKOWAAS_MENU_CACHE="Cache Management" diff --git a/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php b/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php index 648340a8..e7696638 100644 --- a/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php +++ b/src/packages/com_mokowaas/admin/src/Controller/DisplayController.php @@ -30,6 +30,7 @@ class DisplayController extends BaseController 'tickets' => 'mokowaas.tickets', 'ticket' => 'mokowaas.tickets', 'privacy' => 'core.admin', + 'waflog' => 'core.admin', ]; public function display($cachable = false, $urlparams = []) @@ -269,6 +270,40 @@ class DisplayController extends BaseController } } + // ================================================================== + // WAF Log + // ================================================================== + + public function purgeWafLog() + { + Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + + if (!$this->checkAcl('core.admin')) + { + $this->jsonForbidden(); + } + + $days = Factory::getApplication()->getInput()->getInt('days', 30); + $model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel(); + + $this->jsonResponse($model->purgeLogs($days)); + } + + public function banIpFromLog() + { + Session::checkToken() or die(Text::_('JINVALID_TOKEN')); + + if (!$this->checkAcl('core.admin')) + { + $this->jsonForbidden(); + } + + $ip = Factory::getApplication()->getInput()->getString('ip', ''); + $model = new \Moko\Component\MokoWaaS\Administrator\Model\WaflogModel(); + + $this->jsonResponse($model->banIp($ip)); + } + // ================================================================== // Privacy Guard // ================================================================== diff --git a/src/packages/com_mokowaas/admin/src/Model/WaflogModel.php b/src/packages/com_mokowaas/admin/src/Model/WaflogModel.php new file mode 100644 index 00000000..591ba310 --- /dev/null +++ b/src/packages/com_mokowaas/admin/src/Model/WaflogModel.php @@ -0,0 +1,215 @@ +getDatabase(); + $query = $db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__mokowaas_waf_log')); + + if (!empty($filters['rule'])) + { + $query->where($db->quoteName('rule') . ' = ' . $db->quote($filters['rule'])); + } + + if (!empty($filters['ip'])) + { + $query->where($db->quoteName('ip') . ' LIKE ' . $db->quote('%' . $db->escape($filters['ip'], true) . '%')); + } + + if (!empty($filters['search'])) + { + $search = $db->quote('%' . $db->escape($filters['search'], true) . '%'); + $query->where('(' . $db->quoteName('uri') . ' LIKE ' . $search + . ' OR ' . $db->quoteName('detail') . ' LIKE ' . $search + . ' OR ' . $db->quoteName('user_agent') . ' LIKE ' . $search . ')'); + } + + if (!empty($filters['date_from'])) + { + $query->where($db->quoteName('created') . ' >= ' . $db->quote($filters['date_from'] . ' 00:00:00')); + } + + if (!empty($filters['date_to'])) + { + $query->where($db->quoteName('created') . ' <= ' . $db->quote($filters['date_to'] . ' 23:59:59')); + } + + $query->order($db->quoteName('created') . ' DESC'); + $query->setLimit($limit, $offset); + + $db->setQuery($query); + + return $db->loadObjectList() ?: []; + } + + /** + * Get total count for pagination. + */ + public function getTotal(array $filters = []): int + { + $db = $this->getDatabase(); + $query = $db->getQuery(true) + ->select('COUNT(*)') + ->from($db->quoteName('#__mokowaas_waf_log')); + + if (!empty($filters['rule'])) + { + $query->where($db->quoteName('rule') . ' = ' . $db->quote($filters['rule'])); + } + + if (!empty($filters['ip'])) + { + $query->where($db->quoteName('ip') . ' LIKE ' . $db->quote('%' . $db->escape($filters['ip'], true) . '%')); + } + + $db->setQuery($query); + + return (int) $db->loadResult(); + } + + /** + * Get block counts grouped by rule for the summary bar. + */ + public function getRuleCounts(): array + { + $db = $this->getDatabase(); + $db->setQuery( + $db->getQuery(true) + ->select([$db->quoteName('rule'), 'COUNT(*) AS ' . $db->quoteName('cnt')]) + ->from($db->quoteName('#__mokowaas_waf_log')) + ->group($db->quoteName('rule')) + ->order($db->quoteName('cnt') . ' DESC') + ); + + return $db->loadObjectList() ?: []; + } + + /** + * Get top blocked IPs. + */ + public function getTopIps(int $limit = 10): array + { + $db = $this->getDatabase(); + $db->setQuery( + $db->getQuery(true) + ->select([$db->quoteName('ip'), 'COUNT(*) AS ' . $db->quoteName('cnt'), + 'MAX(' . $db->quoteName('created') . ') AS ' . $db->quoteName('last_seen')]) + ->from($db->quoteName('#__mokowaas_waf_log')) + ->group($db->quoteName('ip')) + ->order($db->quoteName('cnt') . ' DESC') + ->setLimit($limit) + ); + + return $db->loadObjectList() ?: []; + } + + /** + * Get distinct rule names for the filter dropdown. + */ + public function getRuleNames(): array + { + $db = $this->getDatabase(); + $db->setQuery( + $db->getQuery(true) + ->select('DISTINCT ' . $db->quoteName('rule')) + ->from($db->quoteName('#__mokowaas_waf_log')) + ->order($db->quoteName('rule') . ' ASC') + ); + + return $db->loadColumn() ?: []; + } + + /** + * Delete logs older than N days. + */ + public function purgeLogs(int $days): array + { + try + { + $db = $this->getDatabase(); + $cutoff = Factory::getDate('-' . $days . ' days')->toSql(); + + $db->setQuery( + $db->getQuery(true) + ->delete($db->quoteName('#__mokowaas_waf_log')) + ->where($db->quoteName('created') . ' < ' . $db->quote($cutoff)) + )->execute(); + + $count = $db->getAffectedRows(); + + return ['success' => true, 'message' => "Purged {$count} log entries older than {$days} days."]; + } + catch (\Throwable $e) + { + return ['success' => false, 'message' => 'Purge failed: ' . $e->getMessage()]; + } + } + + /** + * Add an IP to the firewall blocklist. + */ + public function banIp(string $ip, string $reason = 'Banned from WAF log'): array + { + try + { + $db = $this->getDatabase(); + + $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) ?: []; + + // Check if already blocked + foreach ($blocklist as $entry) + { + if (($entry['ip'] ?? '') === $ip) + { + return ['success' => false, 'message' => $ip . ' is already blocked.']; + } + } + + $blocklist[] = ['ip' => $ip, 'enabled' => '1', 'label' => $reason]; + $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(); + + return ['success' => true, 'message' => $ip . ' has been added to the IP blocklist.']; + } + catch (\Throwable $e) + { + return ['success' => false, 'message' => 'Ban failed: ' . $e->getMessage()]; + } + } +} diff --git a/src/packages/com_mokowaas/admin/src/View/Waflog/HtmlView.php b/src/packages/com_mokowaas/admin/src/View/Waflog/HtmlView.php new file mode 100644 index 00000000..e1f73a9c --- /dev/null +++ b/src/packages/com_mokowaas/admin/src/View/Waflog/HtmlView.php @@ -0,0 +1,55 @@ +getInput(); + + $this->filters = [ + 'rule' => $input->getString('filter_rule', ''), + 'ip' => $input->getString('filter_ip', ''), + 'search' => $input->getString('filter_search', ''), + 'date_from' => $input->getString('filter_date_from', ''), + 'date_to' => $input->getString('filter_date_to', ''), + ]; + + $page = max(1, $input->getInt('page', 1)); + $limit = 50; + $offset = ($page - 1) * $limit; + + $this->logs = $model->getLogs($this->filters, $limit, $offset); + $this->total = $model->getTotal($this->filters); + $this->ruleCounts = $model->getRuleCounts(); + $this->topIps = $model->getTopIps(10); + $this->ruleNames = $model->getRuleNames(); + + $this->addToolbar(); + + $wa = Factory::getApplication()->getDocument()->getWebAssetManager(); + $wa->registerAndUseStyle('com_mokowaas.dashboard', 'com_mokowaas/dashboard.css'); + + parent::display($tpl); + } + + protected function addToolbar(): void + { + ToolbarHelper::title('WAF Log Viewer', 'shield-alt'); + ToolbarHelper::back('JTOOLBAR_BACK', 'index.php?option=com_mokowaas'); + } +} diff --git a/src/packages/com_mokowaas/admin/tmpl/waflog/default.php b/src/packages/com_mokowaas/admin/tmpl/waflog/default.php new file mode 100644 index 00000000..4fab7ab2 --- /dev/null +++ b/src/packages/com_mokowaas/admin/tmpl/waflog/default.php @@ -0,0 +1,212 @@ +logs; +$ruleCounts = $this->ruleCounts; +$topIps = $this->topIps; +$ruleNames = $this->ruleNames; +$total = $this->total; +$filters = $this->filters; +$token = Session::getFormToken(); +$input = Factory::getApplication()->getInput(); +$page = max(1, $input->getInt('page', 1)); +$totalPages = max(1, ceil($total / 50)); + +$ruleBadge = [ + 'sqli' => 'bg-danger', 'xss' => 'bg-danger', 'mua' => 'bg-warning text-dark', + 'rfi' => 'bg-danger', 'dfi' => 'bg-danger', 'blocked_file' => 'bg-info', + 'blocked_php' => 'bg-info', 'tmpl_switch' => 'bg-secondary', + 'ip_blocklist' => 'bg-dark', 'admin_secret' => 'bg-dark', +]; +?> + +
+ +
+ +
+ rule); ?> + cnt); ?> +
+ +
+ Total + +
+
+ +
+ +
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + Reset +
+
+
+
+ + +
+
+ blocked requests + +
+
+ + + + + + + + + + + + + + + + + + + + + +
TimeIPRuleURIDetailUser Agent
No blocked requests found.
created, 'M d H:i:s'); ?>ip); ?>rule); ?>uri, 0, 60)); ?>detail, 0, 50)); ?>user_agent, 0, 40)); ?> + +
+
+ + 1): ?> + + +
+
+ + +
+
+
Top Blocked IPs
+
+ + + + + + + + + + + + +
IPBlocksLast
ip); ?>cnt; ?>last_seen, 'M d'); ?> + +
+
+
+
+
+
+ + diff --git a/src/packages/com_mokowaas/mokowaas.xml b/src/packages/com_mokowaas/mokowaas.xml index 2168186e..cd71e421 100644 --- a/src/packages/com_mokowaas/mokowaas.xml +++ b/src/packages/com_mokowaas/mokowaas.xml @@ -33,6 +33,7 @@ COM_MOKOWAAS_MENU_TICKETS COM_MOKOWAAS_MENU_HTACCESS COM_MOKOWAAS_MENU_PRIVACY + COM_MOKOWAAS_MENU_WAFLOG COM_MOKOWAAS_MENU_PLUGINS COM_MOKOWAAS_MENU_UPDATES COM_MOKOWAAS_MENU_CHECKIN