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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ | Time | IP | Rule | URI | Detail | User Agent | |
+
+
+
+ | No blocked requests found. |
+
+
+
+ | created, 'M d H:i:s'); ?> |
+ ip); ?> |
+ rule); ?> |
+ uri, 0, 60)); ?> |
+ detail, 0, 50)); ?> |
+ user_agent, 0, 40)); ?> |
+
+
+ |
+
+
+
+
+
+
+
+ 1): ?>
+
+
+
+
+
+
+
+
+
+
+
+ | IP | Blocks | Last | |
+
+
+
+ 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 @@
+