@usex/mikrotik-mcp 3.51.1 → 3.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +13 -6
- package/dist/index.js +1 -1
- package/dist/shared/{cli-22wrpg71.js → cli-ewkttc56.js} +90 -110
- package/dist/shared/{cli-dq28zbns.js → cli-thdzk64q.js} +1 -1
- package/dist/shared/{library-510csx1a.js → library-4npjv6jq.js} +1 -1
- package/dist/shared/{library-mwcpnqqg.js → library-n3yvjpkm.js} +90 -110
- package/package.json +1 -1
- package/prompts/audit-security-posture.md +41 -0
- package/prompts/backup-and-document.md +10 -5
- package/prompts/choose-vpn-solution.md +3 -0
- package/prompts/design-qos-policy.md +49 -0
- package/prompts/detect-config-drift.md +51 -0
- package/prompts/diagnose-connectivity.md +15 -5
- package/prompts/disaster-recovery-drill.md +43 -0
- package/prompts/fleet-health-check.md +41 -0
- package/prompts/harden-router.md +10 -0
- package/prompts/manage-certificates.md +51 -0
- package/prompts/safe-change-workflow.md +6 -0
- package/prompts/setup-bgp-peering.md +57 -0
- package/prompts/setup-hotspot.md +52 -0
- package/prompts/setup-multi-wan.md +46 -0
- package/prompts/setup-ospf-peering.md +56 -0
- package/prompts/setup-port-knocking.md +53 -0
- package/prompts/setup-threat-feeds.md +47 -0
- package/prompts/setup-vlan-network.md +47 -0
- package/prompts/troubleshoot-network.md +45 -0
- package/prompts/upgrade-firmware.md +48 -0
|
@@ -5738,7 +5738,7 @@ function renderComplianceReport(report, device, options) {
|
|
|
5738
5738
|
`).trimEnd();
|
|
5739
5739
|
}
|
|
5740
5740
|
|
|
5741
|
-
// src/
|
|
5741
|
+
// src/utils/safe-exec.ts
|
|
5742
5742
|
async function safe(cmd, ctx) {
|
|
5743
5743
|
try {
|
|
5744
5744
|
const out = await executeMikrotikCommand(cmd, ctx);
|
|
@@ -5747,6 +5747,8 @@ async function safe(cmd, ctx) {
|
|
|
5747
5747
|
return "";
|
|
5748
5748
|
}
|
|
5749
5749
|
}
|
|
5750
|
+
|
|
5751
|
+
// src/tools/compliance-audit.ts
|
|
5750
5752
|
async function fetchComplianceState(ctx) {
|
|
5751
5753
|
const [
|
|
5752
5754
|
sshRaw,
|
|
@@ -7104,6 +7106,31 @@ ${created.join(`
|
|
|
7104
7106
|
// src/tools/container.ts
|
|
7105
7107
|
import { z as z17 } from "zod";
|
|
7106
7108
|
|
|
7109
|
+
// src/utils/ip.ts
|
|
7110
|
+
function isIpAddress(s) {
|
|
7111
|
+
return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(s);
|
|
7112
|
+
}
|
|
7113
|
+
function isIpLike(s) {
|
|
7114
|
+
return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d+)?$/.test(s);
|
|
7115
|
+
}
|
|
7116
|
+
function isPrivateIp(ip) {
|
|
7117
|
+
const p = ip.split(".").map(Number);
|
|
7118
|
+
if (p.length !== 4)
|
|
7119
|
+
return false;
|
|
7120
|
+
if (p[0] === 10)
|
|
7121
|
+
return true;
|
|
7122
|
+
if (p[0] === 172 && p[1] >= 16 && p[1] <= 31)
|
|
7123
|
+
return true;
|
|
7124
|
+
if (p[0] === 192 && p[1] === 168)
|
|
7125
|
+
return true;
|
|
7126
|
+
if (p[0] === 169 && p[1] === 254)
|
|
7127
|
+
return true;
|
|
7128
|
+
return false;
|
|
7129
|
+
}
|
|
7130
|
+
// src/utils/num.ts
|
|
7131
|
+
function num(s) {
|
|
7132
|
+
return Number.parseInt(s ?? "0", 10) || 0;
|
|
7133
|
+
}
|
|
7107
7134
|
// src/utils/or-match.ts
|
|
7108
7135
|
function orMatch(field, values, op = "~") {
|
|
7109
7136
|
if (values.length === 0)
|
|
@@ -7875,7 +7902,7 @@ var cache = null;
|
|
|
7875
7902
|
async function gateway() {
|
|
7876
7903
|
if (cache)
|
|
7877
7904
|
return cache;
|
|
7878
|
-
const { moduleCatalog } = await import("./library-
|
|
7905
|
+
const { moduleCatalog } = await import("./library-4npjv6jq.js");
|
|
7879
7906
|
const forIndex = [];
|
|
7880
7907
|
const byName = new Map;
|
|
7881
7908
|
for (const mod of moduleCatalog) {
|
|
@@ -8845,7 +8872,7 @@ function rulesFromRows(rows) {
|
|
|
8845
8872
|
else
|
|
8846
8873
|
match[k] = v;
|
|
8847
8874
|
}
|
|
8848
|
-
const
|
|
8875
|
+
const num2 = (s) => {
|
|
8849
8876
|
if (s == null)
|
|
8850
8877
|
return;
|
|
8851
8878
|
const n = Number(s.replace(/\s/g, ""));
|
|
@@ -8858,8 +8885,8 @@ function rulesFromRows(rows) {
|
|
|
8858
8885
|
disabled: flags.includes("X"),
|
|
8859
8886
|
dynamic: flags.includes("D"),
|
|
8860
8887
|
comment: r.comment,
|
|
8861
|
-
packets:
|
|
8862
|
-
bytes:
|
|
8888
|
+
packets: num2(r.packets),
|
|
8889
|
+
bytes: num2(r.bytes),
|
|
8863
8890
|
match,
|
|
8864
8891
|
transform,
|
|
8865
8892
|
raw: r
|
|
@@ -8928,6 +8955,8 @@ function matchSummary(rule) {
|
|
|
8928
8955
|
"dst-port",
|
|
8929
8956
|
"in-interface",
|
|
8930
8957
|
"out-interface",
|
|
8958
|
+
"in-interface-list",
|
|
8959
|
+
"out-interface-list",
|
|
8931
8960
|
"connection-state",
|
|
8932
8961
|
"src-address-list",
|
|
8933
8962
|
"dst-address-list"
|
|
@@ -9676,11 +9705,12 @@ async function updateFilterRule(a, ctx) {
|
|
|
9676
9705
|
}
|
|
9677
9706
|
if (updates.length === 0)
|
|
9678
9707
|
return "No updates specified.";
|
|
9679
|
-
const
|
|
9708
|
+
const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
|
|
9709
|
+
const cmd = `/ip firewall filter set ${id} ${updates.join(" ")}`;
|
|
9680
9710
|
const result = await executeMikrotikCommand(cmd, ctx);
|
|
9681
9711
|
if (looksLikeError(result))
|
|
9682
9712
|
return `Failed to update firewall filter rule: ${result}`;
|
|
9683
|
-
const details = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${
|
|
9713
|
+
const details = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${id}`, ctx);
|
|
9684
9714
|
return `Firewall filter rule updated successfully:
|
|
9685
9715
|
|
|
9686
9716
|
${details}`;
|
|
@@ -9835,7 +9865,8 @@ ${result}`;
|
|
|
9835
9865
|
},
|
|
9836
9866
|
async handler(a, ctx) {
|
|
9837
9867
|
ctx.info(`Getting firewall filter rule details: rule_id=${a.rule_id}`);
|
|
9838
|
-
const
|
|
9868
|
+
const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
|
|
9869
|
+
const result = await executeMikrotikCommand(`/ip firewall filter print detail where .id=${id}`, ctx);
|
|
9839
9870
|
return isEmpty(result) ? `Firewall filter rule with ID '${a.rule_id}' not found.` : `FIREWALL FILTER RULE DETAILS:
|
|
9840
9871
|
|
|
9841
9872
|
${result}`;
|
|
@@ -9911,10 +9942,11 @@ ${result}`;
|
|
|
9911
9942
|
inputSchema: { rule_id: z28.string() },
|
|
9912
9943
|
async handler(a, ctx) {
|
|
9913
9944
|
ctx.info(`Removing firewall filter rule: rule_id=${a.rule_id}`);
|
|
9914
|
-
const
|
|
9945
|
+
const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
|
|
9946
|
+
const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
|
|
9915
9947
|
if (count.trim() === "0")
|
|
9916
9948
|
return `Firewall filter rule with ID '${a.rule_id}' not found.`;
|
|
9917
|
-
const result = await executeMikrotikCommand(`/ip firewall filter remove ${
|
|
9949
|
+
const result = await executeMikrotikCommand(`/ip firewall filter remove ${id}`, ctx);
|
|
9918
9950
|
if (looksLikeError(result))
|
|
9919
9951
|
return `Failed to remove firewall filter rule: ${result}`;
|
|
9920
9952
|
return `Firewall filter rule with ID '${a.rule_id}' removed successfully.`;
|
|
@@ -9931,10 +9963,11 @@ ${result}`;
|
|
|
9931
9963
|
},
|
|
9932
9964
|
async handler(a, ctx) {
|
|
9933
9965
|
ctx.info(`Moving firewall filter rule: rule_id=${a.rule_id} to position ${a.destination}`);
|
|
9934
|
-
const
|
|
9966
|
+
const id = /^\d+$/.test(a.rule_id) ? `*${a.rule_id}` : a.rule_id;
|
|
9967
|
+
const count = await executeMikrotikCommand(`/ip firewall filter print count-only where .id=${id}`, ctx);
|
|
9935
9968
|
if (count.trim() === "0")
|
|
9936
9969
|
return `Firewall filter rule with ID '${a.rule_id}' not found.`;
|
|
9937
|
-
const result = await executeMikrotikCommand(`/ip firewall filter move ${
|
|
9970
|
+
const result = await executeMikrotikCommand(`/ip firewall filter move ${id} destination=${a.destination}`, ctx);
|
|
9938
9971
|
if (looksLikeError(result))
|
|
9939
9972
|
return `Failed to move firewall filter rule: ${result}`;
|
|
9940
9973
|
return `Firewall filter rule with ID '${a.rule_id}' moved to position ${a.destination}.`;
|
|
@@ -10831,7 +10864,7 @@ function assessUpgradeReadiness(state) {
|
|
|
10831
10864
|
});
|
|
10832
10865
|
}
|
|
10833
10866
|
const cpuNum = Number.parseInt(state.cpuLoad, 10);
|
|
10834
|
-
if (!isNaN(cpuNum) && cpuNum > 80) {
|
|
10867
|
+
if (!Number.isNaN(cpuNum) && cpuNum > 80) {
|
|
10835
10868
|
checks.push({
|
|
10836
10869
|
label: "CPU load",
|
|
10837
10870
|
status: "caution",
|
|
@@ -10844,8 +10877,8 @@ function assessUpgradeReadiness(state) {
|
|
|
10844
10877
|
detail: `${state.cpuLoad}`
|
|
10845
10878
|
});
|
|
10846
10879
|
}
|
|
10847
|
-
const free =
|
|
10848
|
-
const total =
|
|
10880
|
+
const free = parseSize(state.freeMemory) ?? 0;
|
|
10881
|
+
const total = parseSize(state.totalMemory) ?? 0;
|
|
10849
10882
|
if (free > 0 && total > 0) {
|
|
10850
10883
|
const pct2 = Math.round(free / total * 100);
|
|
10851
10884
|
if (pct2 < 20) {
|
|
@@ -10882,20 +10915,6 @@ function assessUpgradeReadiness(state) {
|
|
|
10882
10915
|
}
|
|
10883
10916
|
return checks;
|
|
10884
10917
|
}
|
|
10885
|
-
function parseMem(s) {
|
|
10886
|
-
const m = s.match(/([\d.]+)\s*(MiB|GiB|KiB)?/i);
|
|
10887
|
-
if (!m)
|
|
10888
|
-
return 0;
|
|
10889
|
-
const val = Number.parseFloat(m[1]);
|
|
10890
|
-
const unit = (m[2] ?? "").toLowerCase();
|
|
10891
|
-
if (unit === "gib")
|
|
10892
|
-
return val * 1024 * 1024 * 1024;
|
|
10893
|
-
if (unit === "mib")
|
|
10894
|
-
return val * 1024 * 1024;
|
|
10895
|
-
if (unit === "kib")
|
|
10896
|
-
return val * 1024;
|
|
10897
|
-
return val;
|
|
10898
|
-
}
|
|
10899
10918
|
function renderFirmwareStatus(state, device) {
|
|
10900
10919
|
const lines = [];
|
|
10901
10920
|
lines.push(`FIRMWARE STATUS \u2014 ${device}`);
|
|
@@ -11033,14 +11052,6 @@ function renderReadiness(checks) {
|
|
|
11033
11052
|
}
|
|
11034
11053
|
|
|
11035
11054
|
// src/tools/firmware-lifecycle.ts
|
|
11036
|
-
async function safe2(cmd, ctx) {
|
|
11037
|
-
try {
|
|
11038
|
-
const out = await executeMikrotikCommand(cmd, ctx);
|
|
11039
|
-
return looksLikeError(out) ? "" : out;
|
|
11040
|
-
} catch {
|
|
11041
|
-
return "";
|
|
11042
|
-
}
|
|
11043
|
-
}
|
|
11044
11055
|
function parsePackages(raw) {
|
|
11045
11056
|
if (!raw)
|
|
11046
11057
|
return [];
|
|
@@ -11085,10 +11096,10 @@ function parseUpdateInfo(raw) {
|
|
|
11085
11096
|
}
|
|
11086
11097
|
async function fetchFirmwareState(ctx) {
|
|
11087
11098
|
const [resourceRaw, packagesRaw, routerboardRaw, updateRaw] = await Promise.all([
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11099
|
+
safe("/system resource print", ctx),
|
|
11100
|
+
safe("/system package print detail", ctx),
|
|
11101
|
+
safe("/system routerboard print", ctx),
|
|
11102
|
+
safe("/system package update print", ctx)
|
|
11092
11103
|
]);
|
|
11093
11104
|
const res = parseKeyValues(resourceRaw);
|
|
11094
11105
|
return {
|
|
@@ -11106,12 +11117,12 @@ async function fetchFirmwareState(ctx) {
|
|
|
11106
11117
|
}
|
|
11107
11118
|
async function captureHealthSnapshot(ctx) {
|
|
11108
11119
|
const [resourceRaw, healthRaw, routesRaw, interfacesRaw, pppRaw, dhcpRaw] = await Promise.all([
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11120
|
+
safe("/system resource print", ctx),
|
|
11121
|
+
safe("/system health print", ctx),
|
|
11122
|
+
safe("/ip route print count-only", ctx),
|
|
11123
|
+
safe("/interface print detail", ctx),
|
|
11124
|
+
safe("/ppp active print count-only", ctx),
|
|
11125
|
+
safe("/ip dhcp-server lease print count-only where status=bound", ctx)
|
|
11115
11126
|
]);
|
|
11116
11127
|
const res = parseKeyValues(resourceRaw);
|
|
11117
11128
|
const ifRows = interfacesRaw ? parseRecords(interfacesRaw).rows : [];
|
|
@@ -11211,8 +11222,8 @@ var firmwareLifecycleTools = [
|
|
|
11211
11222
|
if (looksLikeError(chResult))
|
|
11212
11223
|
return `Failed to set channel: ${chResult}`;
|
|
11213
11224
|
}
|
|
11214
|
-
|
|
11215
|
-
const updateRaw = await
|
|
11225
|
+
await executeMikrotikCommand("/system package update check-for-updates once", ctx);
|
|
11226
|
+
const updateRaw = await safe("/system package update print", ctx);
|
|
11216
11227
|
const updateInfo = parseUpdateInfo(updateRaw);
|
|
11217
11228
|
if (!updateInfo?.updateAvailable) {
|
|
11218
11229
|
return `No update available on '${device}'.
|
|
@@ -11226,7 +11237,7 @@ var firmwareLifecycleTools = [
|
|
|
11226
11237
|
if (looksLikeError(dlResult)) {
|
|
11227
11238
|
return `Failed to download update packages: ${dlResult}`;
|
|
11228
11239
|
}
|
|
11229
|
-
const postRaw = await
|
|
11240
|
+
const postRaw = await safe("/system package update print", ctx);
|
|
11230
11241
|
const postStatus = parseKeyValues(postRaw);
|
|
11231
11242
|
const lines = [];
|
|
11232
11243
|
lines.push(`FIRMWARE STAGED \u2014 ${device}`);
|
|
@@ -11259,7 +11270,7 @@ var firmwareLifecycleTools = [
|
|
|
11259
11270
|
return "Upgrade not confirmed. Pass confirm=true to proceed. The device WILL reboot.";
|
|
11260
11271
|
const device = resolveDeviceName(ctx.device);
|
|
11261
11272
|
ctx.info(`Firmware upgrade on '${device}'`);
|
|
11262
|
-
const updateRaw = await
|
|
11273
|
+
const updateRaw = await safe("/system package update print", ctx);
|
|
11263
11274
|
const updateInfo = parseUpdateInfo(updateRaw);
|
|
11264
11275
|
if (!updateInfo?.updateAvailable) {
|
|
11265
11276
|
return `No update available on '${device}'. ` + `Run \`firmware_check\` or \`firmware_stage\` first.
|
|
@@ -11368,7 +11379,7 @@ var firmwareLifecycleTools = [
|
|
|
11368
11379
|
lines.push(` Current: ${state.routerboard.currentFirmware} \u2192 Available: ${state.routerboard.upgradeFirmware}`);
|
|
11369
11380
|
lines.push(" Use `firmware_upgrade` with `upgrade_routerboard=true` to include it.");
|
|
11370
11381
|
}
|
|
11371
|
-
const schedRaw = await
|
|
11382
|
+
const schedRaw = await safe('/system scheduler print detail where name="mcp-firmware-upgrade"', ctx);
|
|
11372
11383
|
if (schedRaw && !isEmpty(schedRaw)) {
|
|
11373
11384
|
lines.push("");
|
|
11374
11385
|
lines.push("PENDING SCHEDULED UPGRADE:");
|
|
@@ -20326,7 +20337,7 @@ function analyzeRootCause(data) {
|
|
|
20326
20337
|
summary: `${boundLeases.length} active DHCP leases`
|
|
20327
20338
|
});
|
|
20328
20339
|
}
|
|
20329
|
-
if (isIpAddress(data.target)) {
|
|
20340
|
+
if (isIpAddress(data.target) && isPrivateIp(data.target)) {
|
|
20330
20341
|
const lease = data.dhcpLeases.find((l) => l.address === data.target);
|
|
20331
20342
|
const arp = data.arpEntries.find((e) => e.address === data.target);
|
|
20332
20343
|
if (!lease && !arp) {
|
|
@@ -20581,7 +20592,7 @@ function correlateRootCauses(data, evidence, causes) {
|
|
|
20581
20592
|
dimensions: ["resources"]
|
|
20582
20593
|
});
|
|
20583
20594
|
}
|
|
20584
|
-
if (isIpAddress(data.target)) {
|
|
20595
|
+
if (isIpAddress(data.target) && isPrivateIp(data.target)) {
|
|
20585
20596
|
const arp = data.arpEntries.find((e) => e.address === data.target);
|
|
20586
20597
|
if (arp && !arp.complete && data.ping?.lossPct === 100) {
|
|
20587
20598
|
causes.push({
|
|
@@ -20636,9 +20647,6 @@ function correlateRootCauses(data, evidence, causes) {
|
|
|
20636
20647
|
});
|
|
20637
20648
|
}
|
|
20638
20649
|
}
|
|
20639
|
-
function isIpAddress(s) {
|
|
20640
|
-
return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(s);
|
|
20641
|
-
}
|
|
20642
20650
|
function formatRule(r) {
|
|
20643
20651
|
const parts = [`chain=${r.chain}`, `action=${r.action}`];
|
|
20644
20652
|
if (r.srcAddress)
|
|
@@ -20798,19 +20806,8 @@ ${footer}`;
|
|
|
20798
20806
|
}
|
|
20799
20807
|
|
|
20800
20808
|
// src/tools/root-cause.ts
|
|
20801
|
-
async function safe3(cmd, ctx) {
|
|
20802
|
-
try {
|
|
20803
|
-
const out = await executeMikrotikCommand(cmd, ctx);
|
|
20804
|
-
return looksLikeError(out) ? "" : out;
|
|
20805
|
-
} catch {
|
|
20806
|
-
return "";
|
|
20807
|
-
}
|
|
20808
|
-
}
|
|
20809
|
-
function num(s) {
|
|
20810
|
-
return Number.parseInt(s ?? "0", 10) || 0;
|
|
20811
|
-
}
|
|
20812
20809
|
async function collectInterfaces(ctx) {
|
|
20813
|
-
const raw = await
|
|
20810
|
+
const raw = await safe("/interface print detail", ctx);
|
|
20814
20811
|
if (!raw)
|
|
20815
20812
|
return [];
|
|
20816
20813
|
return parseRecords(raw).rows.map((r) => ({
|
|
@@ -20828,7 +20825,7 @@ async function collectInterfaces(ctx) {
|
|
|
20828
20825
|
}));
|
|
20829
20826
|
}
|
|
20830
20827
|
async function collectRoutes(ctx) {
|
|
20831
|
-
const raw = await
|
|
20828
|
+
const raw = await safe("/ip route print detail", ctx);
|
|
20832
20829
|
if (!raw)
|
|
20833
20830
|
return { routes: [], count: 0, hasDefault: false };
|
|
20834
20831
|
const rows = parseRecords(raw).rows;
|
|
@@ -20843,7 +20840,7 @@ async function collectRoutes(ctx) {
|
|
|
20843
20840
|
return { routes, count: routes.length, hasDefault };
|
|
20844
20841
|
}
|
|
20845
20842
|
async function collectOspfNeighbors(ctx) {
|
|
20846
|
-
const raw = await
|
|
20843
|
+
const raw = await safe("/routing ospf neighbor print detail", ctx);
|
|
20847
20844
|
if (!raw)
|
|
20848
20845
|
return [];
|
|
20849
20846
|
return parseRecords(raw).rows.map((r) => ({
|
|
@@ -20855,9 +20852,9 @@ async function collectOspfNeighbors(ctx) {
|
|
|
20855
20852
|
}));
|
|
20856
20853
|
}
|
|
20857
20854
|
async function collectBgpPeers(ctx) {
|
|
20858
|
-
const raw = await
|
|
20855
|
+
const raw = await safe("/routing bgp session print detail", ctx);
|
|
20859
20856
|
if (!raw) {
|
|
20860
|
-
const raw2 = await
|
|
20857
|
+
const raw2 = await safe("/routing bgp peer print detail", ctx);
|
|
20861
20858
|
if (!raw2)
|
|
20862
20859
|
return [];
|
|
20863
20860
|
return parseRecords(raw2).rows.map((r) => ({
|
|
@@ -20877,12 +20874,12 @@ async function collectBgpPeers(ctx) {
|
|
|
20877
20874
|
}));
|
|
20878
20875
|
}
|
|
20879
20876
|
async function collectFirewallRules(target, ctx) {
|
|
20880
|
-
const raw = await
|
|
20877
|
+
const raw = await safe("/ip firewall filter print detail", ctx);
|
|
20881
20878
|
if (!raw)
|
|
20882
20879
|
return { matching: [], totalCount: 0 };
|
|
20883
20880
|
const rows = parseRecords(raw).rows;
|
|
20884
20881
|
const all = rows.map(parseFirewallRow);
|
|
20885
|
-
const isIp =
|
|
20882
|
+
const isIp = isIpAddress(target);
|
|
20886
20883
|
const matching = isIp ? all.filter((r) => !r.srcAddress || r.srcAddress === target || !r.dstAddress || r.dstAddress === target) : all;
|
|
20887
20884
|
return { matching, totalCount: all.length };
|
|
20888
20885
|
}
|
|
@@ -20902,13 +20899,13 @@ function parseFirewallRow(r, i) {
|
|
|
20902
20899
|
};
|
|
20903
20900
|
}
|
|
20904
20901
|
async function collectNatRules(ctx) {
|
|
20905
|
-
const raw = await
|
|
20902
|
+
const raw = await safe("/ip firewall nat print detail", ctx);
|
|
20906
20903
|
if (!raw)
|
|
20907
20904
|
return [];
|
|
20908
20905
|
return parseRecords(raw).rows.map(parseFirewallRow);
|
|
20909
20906
|
}
|
|
20910
20907
|
async function collectArp(ctx) {
|
|
20911
|
-
const raw = await
|
|
20908
|
+
const raw = await safe("/ip arp print detail", ctx);
|
|
20912
20909
|
if (!raw)
|
|
20913
20910
|
return [];
|
|
20914
20911
|
return parseRecords(raw).rows.map((r) => ({
|
|
@@ -20920,7 +20917,7 @@ async function collectArp(ctx) {
|
|
|
20920
20917
|
}));
|
|
20921
20918
|
}
|
|
20922
20919
|
async function collectDhcpLeases(ctx) {
|
|
20923
|
-
const raw = await
|
|
20920
|
+
const raw = await safe("/ip dhcp-server lease print detail", ctx);
|
|
20924
20921
|
if (!raw)
|
|
20925
20922
|
return [];
|
|
20926
20923
|
return parseRecords(raw).rows.map((r) => ({
|
|
@@ -20933,7 +20930,7 @@ async function collectDhcpLeases(ctx) {
|
|
|
20933
20930
|
}));
|
|
20934
20931
|
}
|
|
20935
20932
|
async function collectLogs(target, ctx) {
|
|
20936
|
-
const raw = await
|
|
20933
|
+
const raw = await safe('/log print where topics~"error" or topics~"warning" or topics~"critical" or topics~"firewall" or topics~"system"', ctx);
|
|
20937
20934
|
if (!raw)
|
|
20938
20935
|
return [];
|
|
20939
20936
|
const rows = parseRecords(raw).rows;
|
|
@@ -20944,7 +20941,7 @@ async function collectLogs(target, ctx) {
|
|
|
20944
20941
|
}));
|
|
20945
20942
|
}
|
|
20946
20943
|
async function collectTunnels(ctx) {
|
|
20947
|
-
const raw = await
|
|
20944
|
+
const raw = await safe('/interface print detail where type~"gre|ipip|eoip|vxlan|wireguard|ovpn|sstp|pptp|l2tp"', ctx);
|
|
20948
20945
|
if (!raw)
|
|
20949
20946
|
return [];
|
|
20950
20947
|
return parseRecords(raw).rows.map((r) => ({
|
|
@@ -20979,27 +20976,27 @@ async function collectDiagnosticData(target, ctx, dimensions) {
|
|
|
20979
20976
|
logs,
|
|
20980
20977
|
tunnels
|
|
20981
20978
|
] = await Promise.all([
|
|
20982
|
-
dims.has("connectivity") ?
|
|
20979
|
+
dims.has("connectivity") ? safe(`/ping ${quoteValue(target)} count=5`, ctx) : Promise.resolve(""),
|
|
20983
20980
|
dims.has("interfaces") ? collectInterfaces(ctx) : Promise.resolve([]),
|
|
20984
20981
|
dims.has("routing") ? collectRoutes(ctx) : Promise.resolve({ routes: [], count: 0, hasDefault: true }),
|
|
20985
20982
|
dims.has("routing") ? collectOspfNeighbors(ctx) : Promise.resolve([]),
|
|
20986
20983
|
dims.has("routing") ? collectBgpPeers(ctx) : Promise.resolve([]),
|
|
20987
20984
|
dims.has("firewall") ? collectFirewallRules(target, ctx) : Promise.resolve({ matching: [], totalCount: 0 }),
|
|
20988
20985
|
dims.has("nat") ? collectNatRules(ctx) : Promise.resolve([]),
|
|
20989
|
-
dims.has("nat") ?
|
|
20986
|
+
dims.has("nat") ? safe("/ip firewall connection print count-only", ctx) : Promise.resolve("0"),
|
|
20990
20987
|
dims.has("arp_dhcp") ? collectArp(ctx) : Promise.resolve([]),
|
|
20991
20988
|
dims.has("arp_dhcp") ? collectDhcpLeases(ctx) : Promise.resolve([]),
|
|
20992
|
-
dims.has("dns") && !isIpLike(target) ?
|
|
20993
|
-
dims.has("dns") ?
|
|
20994
|
-
dims.has("resources") ?
|
|
20989
|
+
dims.has("dns") && !isIpLike(target) ? safe(`[:resolve ${quoteValue(target)}]`, ctx) : Promise.resolve(undefined),
|
|
20990
|
+
dims.has("dns") ? safe("/ip dns print", ctx) : Promise.resolve(""),
|
|
20991
|
+
dims.has("resources") ? safe("/system resource print", ctx) : Promise.resolve(""),
|
|
20995
20992
|
dims.has("logs") ? collectLogs(target, ctx) : Promise.resolve([]),
|
|
20996
20993
|
dims.has("vpn") ? collectTunnels(ctx) : Promise.resolve([])
|
|
20997
20994
|
]);
|
|
20998
20995
|
const ping = pingResult ? parsePingSummary(pingResult) ?? undefined : undefined;
|
|
20999
20996
|
const dnsKv = parseKeyValues(dnsSettingsRaw);
|
|
21000
20997
|
const resKv = parseKeyValues(resourceRaw);
|
|
21001
|
-
const totalMem =
|
|
21002
|
-
const freeMem =
|
|
20998
|
+
const totalMem = parseSize(resKv["total-memory"]) ?? 0;
|
|
20999
|
+
const freeMem = parseSize(resKv["free-memory"]) ?? 0;
|
|
21003
21000
|
const memUsedPct = totalMem > 0 ? Math.round((totalMem - freeMem) / totalMem * 100) : 0;
|
|
21004
21001
|
return {
|
|
21005
21002
|
target,
|
|
@@ -21018,9 +21015,9 @@ async function collectDiagnosticData(target, ctx, dimensions) {
|
|
|
21018
21015
|
arpEntries,
|
|
21019
21016
|
dhcpLeases,
|
|
21020
21017
|
dnsResolveResult: dnsResult ?? undefined,
|
|
21021
|
-
dnsServers: dnsKv.servers
|
|
21018
|
+
dnsServers: [dnsKv.servers, dnsKv["dynamic-servers"]].filter(Boolean).join(",") || "",
|
|
21022
21019
|
dnsAllowRemote: (dnsKv["allow-remote-requests"] ?? "").toLowerCase() === "yes",
|
|
21023
|
-
cpuLoad:
|
|
21020
|
+
cpuLoad: parsePercent(resKv["cpu-load"]) ?? 0,
|
|
21024
21021
|
memoryUsedPct: memUsedPct,
|
|
21025
21022
|
uptime: resKv.uptime ?? "",
|
|
21026
21023
|
rosVersion: resKv.version ?? "",
|
|
@@ -21028,23 +21025,6 @@ async function collectDiagnosticData(target, ctx, dimensions) {
|
|
|
21028
21025
|
tunnelInterfaces: tunnels
|
|
21029
21026
|
};
|
|
21030
21027
|
}
|
|
21031
|
-
function isIpLike(s) {
|
|
21032
|
-
return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(\/\d+)?$/.test(s);
|
|
21033
|
-
}
|
|
21034
|
-
function parseMemMiB(s) {
|
|
21035
|
-
if (!s)
|
|
21036
|
-
return 0;
|
|
21037
|
-
const m = s.match(/([\d.]+)\s*(MiB|GiB|KiB)?/i);
|
|
21038
|
-
if (!m)
|
|
21039
|
-
return 0;
|
|
21040
|
-
const v = Number.parseFloat(m[1]);
|
|
21041
|
-
const u = (m[2] ?? "").toLowerCase();
|
|
21042
|
-
if (u === "gib")
|
|
21043
|
-
return v * 1024;
|
|
21044
|
-
if (u === "kib")
|
|
21045
|
-
return v / 1024;
|
|
21046
|
-
return v;
|
|
21047
|
-
}
|
|
21048
21028
|
var dimensionEnum = z86.enum(ALL_DIMENSIONS).describe("Diagnostic dimension to investigate.");
|
|
21049
21029
|
var rootCauseTools = [
|
|
21050
21030
|
defineTool({
|
|
@@ -21086,9 +21066,9 @@ var rootCauseTools = [
|
|
|
21086
21066
|
const device = resolveDeviceName(ctx.device);
|
|
21087
21067
|
ctx.info(`Tracing path from '${device}' to ${a.target}`);
|
|
21088
21068
|
const [traceResult, pingResult, routeResult] = await Promise.all([
|
|
21089
|
-
|
|
21090
|
-
|
|
21091
|
-
|
|
21069
|
+
safe(`/tool traceroute ${quoteValue(a.target)} count=${a.count} use-dns=${a.use_dns ? "yes" : "no"}`, ctx),
|
|
21070
|
+
safe(`/ping ${quoteValue(a.target)} count=5`, ctx),
|
|
21071
|
+
safe(`/ip route print where dst-address=0.0.0.0/0`, ctx)
|
|
21092
21072
|
]);
|
|
21093
21073
|
const ping = parsePingSummary(pingResult);
|
|
21094
21074
|
const lines = [];
|
|
@@ -21150,7 +21130,7 @@ var rootCauseTools = [
|
|
|
21150
21130
|
filters.push(`message~"${a.keyword}"`);
|
|
21151
21131
|
}
|
|
21152
21132
|
const where = filters.length > 0 ? ` where ${filters.join(" ")}` : "";
|
|
21153
|
-
const raw = await
|
|
21133
|
+
const raw = await safe(`/log print detail${where}`, ctx);
|
|
21154
21134
|
if (!raw || isEmpty(raw)) {
|
|
21155
21135
|
return `No matching log events found on '${device}' in the last ${a.time_window}.`;
|
|
21156
21136
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: audit-security-posture
|
|
3
|
+
title: Run a compliance security audit
|
|
4
|
+
description: Comprehensive security audit across SSH, services, firewall, users, DNS, certificates, SNMP, hardening, and VPN — scored A+ through F with actionable fix commands.
|
|
5
|
+
arguments:
|
|
6
|
+
- name: categories
|
|
7
|
+
description: Comma-separated categories to audit (e.g. "ssh,firewall,users"). Omit for a full audit across all 9 categories.
|
|
8
|
+
required: false
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
Run a comprehensive security compliance audit on this MikroTik device. The audit
|
|
12
|
+
covers 36 checks across 9 categories — SSH, management services, firewall, user
|
|
13
|
+
accounts, DNS, certificates, SNMP, system hardening, and VPN — and scores the
|
|
14
|
+
device from A+ (excellent) down to F (critical).
|
|
15
|
+
|
|
16
|
+
Categories to audit: {{categories}}
|
|
17
|
+
|
|
18
|
+
Follow this workflow:
|
|
19
|
+
|
|
20
|
+
1. **Run the audit.** Call `run_compliance_audit` (optionally filtered to
|
|
21
|
+
`{{categories}}`). Review the per-check results: each check reports pass, fail,
|
|
22
|
+
or warn with a severity level and a specific fix command.
|
|
23
|
+
|
|
24
|
+
2. **Present the results.** Organize findings by severity (critical → low):
|
|
25
|
+
- The overall grade (A+ through F) and numeric score.
|
|
26
|
+
- A summary table of failed and warning checks.
|
|
27
|
+
- For each failing check: what it found, why it matters, and the exact fix
|
|
28
|
+
command provided by the audit engine.
|
|
29
|
+
|
|
30
|
+
3. **Remediate (with user approval).** If the user wants to fix failing checks,
|
|
31
|
+
call `audit_remediate` with `dry_run=true` first to preview what commands would
|
|
32
|
+
run. Then, with approval, call `audit_remediate` with `dry_run=false` to apply.
|
|
33
|
+
|
|
34
|
+
4. **Re-audit.** After remediation, run `run_compliance_audit` again to confirm
|
|
35
|
+
the score improved and no new issues were introduced.
|
|
36
|
+
|
|
37
|
+
5. **Fleet-wide (optional).** If multiple devices are configured, offer to run
|
|
38
|
+
`audit_fleet` for a consolidated report with aggregate scores and the most
|
|
39
|
+
common failures across the fleet.
|
|
40
|
+
|
|
41
|
+
Do not apply any fixes without user approval. Present the audit report first.
|
|
@@ -9,9 +9,12 @@ Create a restore point for this MikroTik device and then write up a clear,
|
|
|
9
9
|
human-readable summary of how it's configured. This is read-mostly: the only
|
|
10
10
|
change is creating a backup/export.
|
|
11
11
|
|
|
12
|
-
1. **
|
|
12
|
+
1. **Local snapshot (zero device footprint)** — `capture_config_snapshot` first.
|
|
13
|
+
This stores a text `/export` in the MCP host's local database — no file is
|
|
14
|
+
written on the router's flash. Give it a descriptive `label` (e.g. `pre-audit`).
|
|
15
|
+
2. **Device restore point** — `create_backup` (binary, for full restore) and
|
|
13
16
|
`create_export` (text `.rsc`, for review/diff). List them with `list_backups`.
|
|
14
|
-
|
|
17
|
+
3. **Inventory** — gather the configuration with read tools and organize it:
|
|
15
18
|
- System: `get_system_identity`, `get_system_resources`, `get_routerboard`,
|
|
16
19
|
`get_installed_packages`.
|
|
17
20
|
- L2/L3: `list_interfaces`, `list_vlan_interfaces`, `list_bridges`,
|
|
@@ -23,10 +26,12 @@ change is creating a backup/export.
|
|
|
23
26
|
`list_users`, `list_certificates`.
|
|
24
27
|
- VPN/QoS: `list_wireguard_interfaces` + `list_wireguard_peers`,
|
|
25
28
|
`list_simple_queues`, `list_queue_trees`.
|
|
29
|
+
- Containers: `list_containers` (if the container feature is enabled).
|
|
26
30
|
- Automation: `list_schedulers`, `list_scripts`.
|
|
27
31
|
|
|
28
32
|
Produce a structured Markdown report: a one-paragraph overview, a table of
|
|
29
33
|
interfaces and addressing, the firewall posture, and a "things worth reviewing"
|
|
30
|
-
section (defaults left in place, disabled-but-present rules, expiring certs
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
section (defaults left in place, disabled-but-present rules, expiring certs —
|
|
35
|
+
check `list_certificates` for any expiring within 30 days).
|
|
36
|
+
Reference the snapshot id and backup/export filenames you created so the user
|
|
37
|
+
knows their restore points.
|
|
@@ -23,6 +23,9 @@ Decision guidance — weigh these MikroTik options:
|
|
|
23
23
|
- **WireGuard** — fastest, simplest, modern. Best for MikroTik↔MikroTik and
|
|
24
24
|
laptops/phones with the WireGuard app. No built-in OS client on older systems.
|
|
25
25
|
Tools: `create_wireguard_interface`, `add_wireguard_peer`, `generate_wireguard_client_config`.
|
|
26
|
+
For **3+ sites** (full-mesh or hub-spoke): `build_wireguard_mesh`.
|
|
27
|
+
For **user onboarding** (generate config, add peer, revoke later):
|
|
28
|
+
`onboard_wireguard_user`, `revoke_wireguard_user`.
|
|
26
29
|
- **IPsec (IKEv2)** — the interoperability choice for site-to-site with
|
|
27
30
|
_other vendors_ (Cisco/Fortinet/pfSense) and for native iOS/Windows IKEv2
|
|
28
31
|
road-warrior. Most config surface. Tools: `create_ipsec_*` (profile/peer/
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-qos-policy
|
|
3
|
+
title: Design a traffic shaping policy
|
|
4
|
+
description: Build a QoS queue hierarchy from business requirements — define traffic classes, priorities, and bandwidth guarantees with preview before apply.
|
|
5
|
+
arguments:
|
|
6
|
+
- name: wan_bandwidth
|
|
7
|
+
description: WAN bandwidth in download/upload format (e.g. "100M/50M", "1G/1G").
|
|
8
|
+
required: true
|
|
9
|
+
- name: classes
|
|
10
|
+
description: Describe traffic classes and priorities (e.g. "VoIP=highest, video conferencing=high, web=normal, bulk downloads=low"). If omitted, a sensible default is used.
|
|
11
|
+
required: false
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Design and deploy a traffic shaping (QoS) policy on this MikroTik device that
|
|
15
|
+
prioritizes important traffic and prevents bulk downloads from saturating the
|
|
16
|
+
link. All changes are previewed before applying.
|
|
17
|
+
|
|
18
|
+
WAN bandwidth: {{wan_bandwidth}}
|
|
19
|
+
Traffic classes: {{classes}}
|
|
20
|
+
|
|
21
|
+
Steps:
|
|
22
|
+
|
|
23
|
+
1. **Understand the network.** Call `list_interfaces` and `list_ip_addresses` to
|
|
24
|
+
identify the WAN interface. Call `list_queue_trees` and `list_simple_queues`
|
|
25
|
+
to check if any QoS is already in place.
|
|
26
|
+
|
|
27
|
+
2. **Design the policy.** Translate {{classes}} into a queue hierarchy:
|
|
28
|
+
- A parent queue on the WAN interface capped at {{wan_bandwidth}}.
|
|
29
|
+
- Child queues for each traffic class with appropriate priorities,
|
|
30
|
+
guaranteed minimum bandwidth, and burst settings.
|
|
31
|
+
Call `apply_traffic_shaping` in **preview mode** to see the queue tree
|
|
32
|
+
structure before applying.
|
|
33
|
+
|
|
34
|
+
3. **Review the queue tree.** Present a table showing:
|
|
35
|
+
- Each traffic class, its priority level, guaranteed bandwidth, max bandwidth.
|
|
36
|
+
- Packet marks or connection marks used to classify traffic.
|
|
37
|
+
- Mangle rules that mark traffic into classes.
|
|
38
|
+
|
|
39
|
+
4. **Apply.** With user approval, deploy the QoS policy.
|
|
40
|
+
|
|
41
|
+
5. **Verify.** Call `list_queue_trees` to confirm the hierarchy is in place.
|
|
42
|
+
Monitor briefly to see traffic flowing through the queues.
|
|
43
|
+
|
|
44
|
+
6. **Capacity planning (optional).** Call `forecast_link_saturation` on the WAN
|
|
45
|
+
interface to project when the current link will reach capacity at the observed
|
|
46
|
+
growth rate — useful for planning upgrades.
|
|
47
|
+
|
|
48
|
+
Report the complete QoS policy: queue hierarchy, classification rules, and
|
|
49
|
+
bandwidth allocations per class.
|