feat(security): Security Center — firewall, NAT, bandwidth, posture, connections (v0.14.0-beta)
A dedicated top-level Security section plus a Meraki/UniFi-grade firewall built
on the full RouterOS feature set.
Security Center (new /security page)
- Fleet hardening scores, per-device posture list, and a "Common Findings"
rollup aggregating identical issues across the fleet
- "Manage" deep-links into the device's Security tab (?tab= support added)
Firewall & NAT (per device + in Security Center)
- Friendly rule builder: Allow/Drop/Reject chips, Any/Address/Address-List
pickers, port presets, connection-state chips, logging, live plain-English
preview
- Address lists as reusable objects, referenced from rules
- Rule reordering (/ip/firewall/{filter,nat}/move); per-rule hit counters
(print stats) with dead-rule highlighting + reset
- NAT wizards: Port Forward / Masquerade / 1:1 NAT + Custom
- Safe-apply lockout guard: backend 409 on an unscoped input drop/reject that
would sever management; UI confirms before forcing (detectLockoutRisk, tested)
Access & security
- Security posture audit: graduated, de-duplicated scoring that never proposes
disabling the management service the platform connects through (no self-
lockout); switch-aware input-firewall severity; service hardening table
- Bandwidth: simple-queue CRUD + one-click "Limit this client" on client detail
- Active connections viewer
These L3 features are available on all managed RouterOS devices (routers,
switches, APs), not gated by device type.
Backend: DeviceCollector address-list/move/stats/connections/queues/service
methods; device routes for all of the above; firewallSafety + firewallSummary
utils with unit tests (19 new).
Security: bump nodemailer 8 -> 9.0.1 to clear GHSA-p6gq-j5cr-w38f (our
createTransport/sendMail usage is unaffected by the advisory's raw-option path).
Verified live against RouterOS devices; lint/typecheck/build/jest green in both
workspaces; npm audit --audit-level=high clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
623e54120d
commit
ee9c9ff9f7
@@ -1747,8 +1747,10 @@ export class DeviceCollector {
|
||||
return this.client.execute('/ip/route/print', { detail: '' });
|
||||
}
|
||||
|
||||
// `stats` makes RouterOS include the per-rule bytes/packets hit counters so
|
||||
// the UI can show which rules are matching (and flag dead, zero-hit rules).
|
||||
async getFirewallRules(): Promise<Record<string, string>[]> {
|
||||
return this.client.execute('/ip/firewall/filter/print', { detail: '' });
|
||||
return this.client.execute('/ip/firewall/filter/print', { detail: '', stats: '' });
|
||||
}
|
||||
|
||||
async addFirewallRule(params: Record<string, string>): Promise<void> {
|
||||
@@ -1763,8 +1765,21 @@ export class DeviceCollector {
|
||||
await this.client.execute('/ip/firewall/filter/remove', { '.id': id });
|
||||
}
|
||||
|
||||
// Reorder: RouterOS evaluates rules top-to-bottom, so order is decisive.
|
||||
// `destination` is the .id the rule should be placed *before* (RouterOS
|
||||
// semantics), or omitted to move to the end.
|
||||
async moveFirewallRule(id: string, destination?: string): Promise<void> {
|
||||
const params: Record<string, string> = { numbers: id };
|
||||
if (destination) params.destination = destination;
|
||||
await this.client.execute('/ip/firewall/filter/move', params);
|
||||
}
|
||||
|
||||
async resetFirewallCounters(): Promise<void> {
|
||||
await this.client.execute('/ip/firewall/filter/reset-counters-all', {});
|
||||
}
|
||||
|
||||
async getNatRules(): Promise<Record<string, string>[]> {
|
||||
return this.client.execute('/ip/firewall/nat/print', { detail: '' });
|
||||
return this.client.execute('/ip/firewall/nat/print', { detail: '', stats: '' });
|
||||
}
|
||||
|
||||
async addNatRule(params: Record<string, string>): Promise<void> {
|
||||
@@ -1779,6 +1794,64 @@ export class DeviceCollector {
|
||||
await this.client.execute('/ip/firewall/nat/remove', { '.id': id });
|
||||
}
|
||||
|
||||
async moveNatRule(id: string, destination?: string): Promise<void> {
|
||||
const params: Record<string, string> = { numbers: id };
|
||||
if (destination) params.destination = destination;
|
||||
await this.client.execute('/ip/firewall/nat/move', params);
|
||||
}
|
||||
|
||||
// ─── Firewall Address Lists (reusable address objects) ──────────────────────
|
||||
|
||||
async getAddressLists(): Promise<Record<string, string>[]> {
|
||||
return this.client.execute('/ip/firewall/address-list/print', { detail: '' }).catch(() => [] as Record<string, string>[]);
|
||||
}
|
||||
|
||||
async addAddressListEntry(params: Record<string, string>): Promise<void> {
|
||||
await this.client.execute('/ip/firewall/address-list/add', params);
|
||||
}
|
||||
|
||||
async updateAddressListEntry(id: string, params: Record<string, string>): Promise<void> {
|
||||
await this.client.execute('/ip/firewall/address-list/set', { '.id': id, ...params });
|
||||
}
|
||||
|
||||
async removeAddressListEntry(id: string): Promise<void> {
|
||||
await this.client.execute('/ip/firewall/address-list/remove', { '.id': id });
|
||||
}
|
||||
|
||||
// ─── Connection tracking (live active connections) ──────────────────────────
|
||||
|
||||
async getConnections(): Promise<Record<string, string>[]> {
|
||||
return this.client.execute('/ip/firewall/connection/print', { detail: '' }).catch(() => [] as Record<string, string>[]);
|
||||
}
|
||||
|
||||
// ─── Simple Queues (per-client / per-subnet bandwidth control) ──────────────
|
||||
|
||||
async getSimpleQueues(): Promise<Record<string, string>[]> {
|
||||
return this.client.execute('/queue/simple/print', { detail: '', stats: '' }).catch(() => [] as Record<string, string>[]);
|
||||
}
|
||||
|
||||
async addSimpleQueue(params: Record<string, string>): Promise<void> {
|
||||
await this.client.execute('/queue/simple/add', params);
|
||||
}
|
||||
|
||||
async updateSimpleQueue(id: string, params: Record<string, string>): Promise<void> {
|
||||
await this.client.execute('/queue/simple/set', { '.id': id, ...params });
|
||||
}
|
||||
|
||||
async removeSimpleQueue(id: string): Promise<void> {
|
||||
await this.client.execute('/queue/simple/remove', { '.id': id });
|
||||
}
|
||||
|
||||
// ─── IP Services (for security-posture audit + hardening) ───────────────────
|
||||
|
||||
async getServices(): Promise<Record<string, string>[]> {
|
||||
return this.client.execute('/ip/service/print', { detail: '' }).catch(() => [] as Record<string, string>[]);
|
||||
}
|
||||
|
||||
async setServiceDisabled(id: string, disabled: boolean): Promise<void> {
|
||||
await this.client.execute('/ip/service/set', { '.id': id, disabled: disabled ? 'yes' : 'no' });
|
||||
}
|
||||
|
||||
// ─── Bridge ────────────────────────────────────────────────────────────────
|
||||
|
||||
async setBridgeVlanFiltering(bridgeName: string, enabled: boolean): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user