diff --git a/CHANGELOG.md b/CHANGELOG.md index d804197..798ab90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Format mengikuti [Keep a Changelog](https://keepachangelog.com/) dan [Semantic Versioning](https://semver.org/). +## [0.6.17] — 2026-11-06 — "security-block" + +### Ditambahkan +- **Auto-block brute force MikroTik** (opsional, default nonaktif): + - percobaan login gagal berulang dimasukkan ke firewall `BLACKLIST_BRUTEFORCE` selama 1 hari, + - rule `drop` untuk chain `input` dan `forward` ditambahkan otomatis, + - hotspot brute force juga memblokir MAC penyerang via Hotspot IP-Binding (`type: blocked`). +- Ambang percobaan dapat diatur di **Pengaturan → Keamanan**. + ## [0.6.16] — 2026-11-06 — "wifi-sync" ### Diperbaiki diff --git a/README.md b/README.md index 72f11ac..7b0c07a 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@
-
+
diff --git a/package.json b/package.json
index 1f13523..9603b58 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "open-isp",
- "version": "0.6.16",
+ "version": "0.6.17",
"description": "OPEN-ISP \u2014 sistem billing & manajemen ISP open-source (PWA pelanggan, multi-OLT, RADIUS, WhatsApp, QRIS)",
"main": "app-customer.js",
"scripts": {
diff --git a/services/securityWatchdogService.js b/services/securityWatchdogService.js
index d42c432..50c3da3 100644
--- a/services/securityWatchdogService.js
+++ b/services/securityWatchdogService.js
@@ -180,7 +180,8 @@ class SecurityWatchdogService {
processedSet.add(logId);
const msg = String(l.message || '');
- if (msg.toLowerCase().includes('login failure') || msg.toLowerCase().includes('failure')) {
+ const low = msg.toLowerCase();
+ if (low.includes('login failure') || low.includes('failure') || low.includes('login failed')) {
failures.push(l);
}
}
@@ -200,19 +201,31 @@ class SecurityWatchdogService {
const match = msg.match(/login failure for user\s+(\S+)\s+from\s+([0-9a-fA-F.:]+)(?:\s+via\s+(\S+))?/i) ||
msg.match(/failure.*user\s+(\S+).*from\s+([0-9a-fA-F.:]+)/i);
- if (match) {
- const targetUser = match[1] || 'unknown';
- const attackerIp = String(match[2] || '').replace(/^::ffff:/, '').trim();
- const service = match[3] || 'router-service';
+ // Format hotspot: "10.5.50.2 (26:BD:26:2A:C3:47): login failed: invalid username or password"
+ const hotspotMatch = msg.match(/(?:->:\s*)?([0-9a-fA-F.:]+)\s*\(([0-9a-fA-F:]+)\):\s*login failed/i);
- if (!attackerIp || attackerIp === '127.0.0.1' || attackerIp === 'localhost') continue;
+ let targetUser, attackerIp, service, mac;
+ if (hotspotMatch) {
+ attackerIp = String(hotspotMatch[1] || '').replace(/^::ffff:/, '').trim();
+ mac = String(hotspotMatch[2] || '').toUpperCase();
+ targetUser = 'voucher-hotspot';
+ service = 'hotspot';
+ } else if (match) {
+ targetUser = match[1] || 'unknown';
+ attackerIp = String(match[2] || '').replace(/^::ffff:/, '').trim();
+ service = match[3] || 'router-service';
+ }
+
+ if (attackerIp) {
+ if (attackerIp === '127.0.0.1' || attackerIp === 'localhost') continue;
if (!failuresByIp.has(attackerIp)) {
- failuresByIp.set(attackerIp, { count: 1, targets: new Set([targetUser]), service });
+ failuresByIp.set(attackerIp, { count: 1, targets: new Set([targetUser]), service, macs: new Set(mac ? [mac] : []) });
} else {
const item = failuresByIp.get(attackerIp);
item.count++;
item.targets.add(targetUser);
+ if (mac) item.macs.add(mac);
}
}
}
@@ -257,6 +270,16 @@ class SecurityWatchdogService {
await this._notifyAdmin(waMsg);
+ // 2b. AUTO-BLOCK (opsional, default nonaktif)
+ if (String(getSetting('security_auto_block_enabled', 'false')) === 'true' && data.count >= (parseInt(getSetting('security_mikrotik_threshold', 5), 10) || 5)) {
+ try {
+ const actions = await this._blockMikrotikIp(conn, attackerIp, r.name, data);
+ if (actions.length) logger.warn('[SecurityWatchdog] Auto-block ' + attackerIp + ': ' + actions.join('; '));
+ } catch (blockErr) {
+ logger.error('[SecurityWatchdog] Auto-block gagal: ' + blockErr.message);
+ }
+ }
+
// 3. CATAT KE AUDIT TRAIL
this._recordAudit({
action: 'ROUTER_BRUTE_FORCE',
@@ -284,6 +307,59 @@ class SecurityWatchdogService {
}
}
+ /**
+ * Blokir IP penyerang di MikroTik (firewall address-list + drop rule).
+ * Hanya dipanggil bila security_auto_block_enabled=true.
+ */
+ async _blockMikrotikIp(conn, ip, routerName, data) {
+ const actions = [];
+ const safeIp = String(ip || '').trim();
+ if (!safeIp || safeIp === '127.0.0.1' || safeIp === 'localhost') return actions;
+ // Hotspot brute force: blokir juga MAC di IP-Binding
+ if (data.macs && data.macs.size) {
+ const bindMenu = conn.client.menu('/ip/hotspot/ip-binding');
+ const bindings = await bindMenu.get();
+ for (const mac of data.macs) {
+ const found = bindings.find(b => String(b['mac-address'] || b.macAddress || '').toUpperCase() === mac);
+ if (!found) {
+ await bindMenu.add({ macAddress: mac, type: 'blocked', comment: 'AI_BLOCKED: hotspot brute force (' + data.count + 'x)' }).catch(function () {});
+ actions.push('MAC ' + mac + ' diblokir di Hotspot IP-Binding');
+ }
+ }
+ }
+
+ const listMenu = conn.client.menu('/ip/firewall/address-list');
+ const rows = await listMenu.get();
+ const exists = rows.find(a => a.address === safeIp && (a.list === 'BLACKLIST_BRUTEFORCE' || a.list === 'BLACKLIST_ANOMALY'));
+ if (!exists) {
+ await listMenu.add({
+ list: 'BLACKLIST_BRUTEFORCE',
+ address: safeIp,
+ timeout: '1d',
+ comment: 'AI_BLOCKED: ' + String(data.service || 'router').toUpperCase() + ' (' + data.count + 'x gagal)'
+ });
+ actions.push('IP ' + safeIp + ' masuk BLACKLIST_BRUTEFORCE (1 hari)');
+ }
+
+ // Pastikan rule drop pada chain input & forward
+ const filterMenu = conn.client.menu('/ip/firewall/filter');
+ const filters = await filterMenu.get();
+ const hasDrop = (chain) => filters.some(f => f.chain === chain && f.action === 'drop' &&
+ (f['src-address-list'] === 'BLACKLIST_BRUTEFORCE' || f.srcAddressList === 'BLACKLIST_BRUTEFORCE'));
+ for (const chain of ['input', 'forward']) {
+ if (!hasDrop(chain)) {
+ await filterMenu.add({
+ chain,
+ action: 'drop',
+ srcAddressList: 'BLACKLIST_BRUTEFORCE',
+ comment: 'AI_AUTO_DROP_BLACKLIST_BRUTEFORCE'
+ }).catch(function () {});
+ actions.push('rule drop ' + chain + ' ditambahkan');
+ }
+ }
+ return actions;
+ }
+
/**
* Ringkasan status keamanan saat ini
*/
diff --git a/version.txt b/version.txt
index a12cadd..ec92ab1 100644
--- a/version.txt
+++ b/version.txt
@@ -1 +1 @@
-0.6.16
\ No newline at end of file
+0.6.17
\ No newline at end of file
diff --git a/views/admin/settings.ejs b/views/admin/settings.ejs
index b8c2397..3e05944 100644
--- a/views/admin/settings.ejs
+++ b/views/admin/settings.ejs
@@ -564,6 +564,20 @@
+