@owine/unifi-network-mcp 2.15.0 → 3.0.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/LICENSE +21 -0
- package/README.md +226 -73
- package/dist/client.d.ts +13 -1
- package/dist/client.js +142 -16
- package/dist/config.d.ts +11 -0
- package/dist/config.js +102 -3
- package/dist/guard.d.ts +46 -0
- package/dist/guard.js +171 -0
- package/dist/index.js +15 -4
- package/dist/instructions.d.ts +9 -0
- package/dist/instructions.js +32 -0
- package/dist/tls.d.ts +14 -0
- package/dist/tls.js +58 -0
- package/dist/tools/acl.js +123 -50
- package/dist/tools/client-history.js +20 -10
- package/dist/tools/clients.js +10 -9
- package/dist/tools/devices.js +21 -20
- package/dist/tools/dns-policies.js +27 -27
- package/dist/tools/firewall.d.ts +2 -1
- package/dist/tools/firewall.js +200 -75
- package/dist/tools/hotspot.js +35 -28
- package/dist/tools/index.d.ts +13 -1
- package/dist/tools/index.js +63 -4
- package/dist/tools/networks.d.ts +2 -1
- package/dist/tools/networks.js +44 -31
- package/dist/tools/sites.js +2 -2
- package/dist/tools/supporting.js +22 -21
- package/dist/tools/switching.js +16 -15
- package/dist/tools/system.js +1 -1
- package/dist/tools/traffic-matching.d.ts +2 -1
- package/dist/tools/traffic-matching.js +22 -20
- package/dist/tools/wifi.js +76 -30
- package/dist/utils/full-put.d.ts +11 -2
- package/dist/utils/full-put.js +25 -2
- package/dist/utils/output-schemas.d.ts +26 -4
- package/dist/utils/output-schemas.js +80 -48
- package/dist/utils/params.d.ts +7 -0
- package/dist/utils/params.js +12 -0
- package/dist/utils/responses.d.ts +2 -0
- package/dist/utils/responses.js +28 -2
- package/dist/utils/safety.d.ts +35 -1
- package/dist/utils/safety.js +53 -3
- package/dist/utils/secrets.js +3 -0
- package/dist/utils/wan-exposure.d.ts +69 -0
- package/dist/utils/wan-exposure.js +245 -0
- package/dist/write-log.d.ts +52 -0
- package/dist/write-log.js +178 -0
- package/dist/write-tools.d.ts +27 -0
- package/dist/write-tools.js +50 -0
- package/package.json +17 -13
package/dist/tools/firewall.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerFirewallTools = registerFirewallTools;
|
|
4
|
+
const node_util_1 = require("node:util");
|
|
4
5
|
const zod_1 = require("zod");
|
|
5
6
|
const responses_js_1 = require("../utils/responses.js");
|
|
6
7
|
const query_js_1 = require("../utils/query.js");
|
|
7
8
|
const safety_js_1 = require("../utils/safety.js");
|
|
8
9
|
const full_put_js_1 = require("../utils/full-put.js");
|
|
9
10
|
const output_schemas_js_1 = require("../utils/output-schemas.js");
|
|
11
|
+
const params_js_1 = require("../utils/params.js");
|
|
12
|
+
const wan_exposure_js_1 = require("../utils/wan-exposure.js");
|
|
10
13
|
// Firewall policy request-body fields, from the 11.0.83 POST/PUT schemas.
|
|
11
14
|
const FIREWALL_POLICY_FIELDS = [
|
|
12
15
|
"enabled",
|
|
@@ -31,27 +34,79 @@ const FIREWALL_POLICY_REQUIRED = [
|
|
|
31
34
|
"ipProtocolScope",
|
|
32
35
|
"loggingEnabled",
|
|
33
36
|
];
|
|
37
|
+
// PATCH fields that change what is logged or reported, never what passes.
|
|
38
|
+
const REPORTING_FIELDS = ["loggingEnabled", "reportFlowEnabled"];
|
|
34
39
|
// Present in responses but not part of the request body.
|
|
35
40
|
const FIREWALL_POLICY_READ_ONLY = ["id", "index", "metadata"];
|
|
41
|
+
// The fields that decide which traffic a policy applies to, and how.
|
|
42
|
+
const FIREWALL_POLICY_MATCH_FIELDS = [
|
|
43
|
+
"action", "source", "destination", "ipProtocolScope", "connectionStateFilter", "ipsecFilter", "schedule",
|
|
44
|
+
];
|
|
45
|
+
/**
|
|
46
|
+
* Whether a policy lets traffic through: any action but BLOCK or REJECT,
|
|
47
|
+
* including a missing one. Compared without case, since policy is a free-form
|
|
48
|
+
* object the controller may accept "allow" in.
|
|
49
|
+
*/
|
|
50
|
+
function letsThrough(policy) {
|
|
51
|
+
const action = policy.action;
|
|
52
|
+
const type = action !== null && typeof action === "object" && "type" in action ? action.type : undefined;
|
|
53
|
+
return typeof type !== "string" || !["BLOCK", "REJECT"].includes(type.toUpperCase());
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* What a firewall policy write would open, if anything. before is undefined
|
|
57
|
+
* on create. Changing what an enabled policy matches counts whatever its
|
|
58
|
+
* action: widening an ALLOW and narrowing a BLOCK both let more through, and
|
|
59
|
+
* which of the two a change does cannot be told from here.
|
|
60
|
+
*/
|
|
61
|
+
function firewallOpening(before, after) {
|
|
62
|
+
if (before === undefined) {
|
|
63
|
+
return letsThrough(after) ? "This creates a firewall policy that ALLOWs traffic" : undefined;
|
|
64
|
+
}
|
|
65
|
+
if (letsThrough(after) && !letsThrough(before))
|
|
66
|
+
return "This changes the firewall policy's action to ALLOW";
|
|
67
|
+
if (after.enabled === false) {
|
|
68
|
+
return before.enabled !== false && !letsThrough(before) ? "This disables a firewall policy that blocks traffic" : undefined;
|
|
69
|
+
}
|
|
70
|
+
if (before.enabled === false) {
|
|
71
|
+
return letsThrough(after) ? "This enables a firewall policy that ALLOWs traffic" : undefined;
|
|
72
|
+
}
|
|
73
|
+
const changed = FIREWALL_POLICY_MATCH_FIELDS.filter((field) => !(0, node_util_1.isDeepStrictEqual)(before[field], after[field]));
|
|
74
|
+
return changed.length > 0
|
|
75
|
+
? `This changes what an enabled firewall policy matches (${changed.join(", ")}), which can let more traffic through`
|
|
76
|
+
: undefined;
|
|
77
|
+
}
|
|
78
|
+
// Stands for the system-defined policies in an evaluation order; not an ID
|
|
79
|
+
// the API can return, since IDs never contain spaces.
|
|
80
|
+
const SYSTEM_DEFINED = "system defined";
|
|
81
|
+
/** A zone pair's policies in evaluation order, the system-defined ones as a single entry. */
|
|
82
|
+
function evaluationOrder(ordering) {
|
|
83
|
+
const ids = (list) => Array.isArray(list) ? list.filter((id) => typeof id === "string") : [];
|
|
84
|
+
const lists = ordering !== null && typeof ordering === "object" ? ordering : {};
|
|
85
|
+
return [...ids(lists.beforeSystemDefined), SYSTEM_DEFINED, ...ids(lists.afterSystemDefined)];
|
|
86
|
+
}
|
|
87
|
+
const confirmOpening = zod_1.z
|
|
88
|
+
.boolean()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe("Set true to confirm a change that can let traffic through: an ALLOW action, enabling an ALLOW policy, disabling a policy that blocks, or changing what an enabled policy matches; not needed with dryRun: true");
|
|
36
91
|
const FIREWALL_POLICY_BODY_DOC = "required enabled, name (non-empty), action ({type: ALLOW|BLOCK, allowReturnTraffic? on ALLOW}), source and destination ({zoneId, trafficFilter?}), ipProtocolScope ({ipVersion, protocolFilter?}), loggingEnabled; optional description, connectionStateFilter (array of NEW/INVALID/ESTABLISHED/RELATED), ipsecFilter (MATCH_ENCRYPTED/MATCH_NOT_ENCRYPTED), schedule (null = always active), reportFlowEnabled (report matches in Insights Flows; defaults to true when omitted).";
|
|
37
|
-
function registerFirewallTools(server, client, readOnly = false) {
|
|
92
|
+
function registerFirewallTools(server, client, readOnly = false, wan = wan_exposure_js_1.DEFAULT_WAN_POLICY) {
|
|
38
93
|
server.registerTool("unifi_list_firewall_zones", {
|
|
39
94
|
description: "List firewall zones (groupings of networks for zone-based firewalling) at a site. Returns: id, name, networkIds[], metadata.origin (indicates system-defined vs user-defined). Use for: zone inventory; pair with unifi_list_firewall_policies to see rules between zones.",
|
|
40
95
|
inputSchema: {
|
|
41
|
-
siteId:
|
|
96
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
42
97
|
offset: zod_1.z
|
|
43
98
|
.number()
|
|
44
99
|
.int()
|
|
45
100
|
.nonnegative()
|
|
46
101
|
.optional()
|
|
47
|
-
.describe("Number of records to skip
|
|
102
|
+
.describe("Number of records to skip"),
|
|
48
103
|
limit: zod_1.z
|
|
49
104
|
.number()
|
|
50
105
|
.int()
|
|
51
106
|
.min(0)
|
|
52
107
|
.max(200)
|
|
53
108
|
.optional()
|
|
54
|
-
.describe("Number of records to return (default: 25
|
|
109
|
+
.describe("Number of records to return (API default: 25)"),
|
|
55
110
|
filter: zod_1.z
|
|
56
111
|
.string()
|
|
57
112
|
.optional()
|
|
@@ -72,8 +127,8 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
72
127
|
server.registerTool("unifi_get_firewall_zone", {
|
|
73
128
|
description: "Get a firewall zone by ID (same fields as the list entry).",
|
|
74
129
|
inputSchema: {
|
|
75
|
-
siteId:
|
|
76
|
-
firewallZoneId:
|
|
130
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
131
|
+
firewallZoneId: (0, params_js_1.resourceId)().describe("Firewall zone ID"),
|
|
77
132
|
},
|
|
78
133
|
outputSchema: output_schemas_js_1.firewallZoneOutputSchema,
|
|
79
134
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -89,20 +144,20 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
89
144
|
server.registerTool("unifi_list_firewall_policies", {
|
|
90
145
|
description: "List firewall policies (zone-based rules) at a site. Returns: id, name, enabled, action (object with type field), source/destination (zone reference + trafficFilter), ipProtocolScope, connectionStateFilter, ipsecFilter, schedule, loggingEnabled, reportFlowEnabled (Insights Flows reporting; false on the predefined Allow Return Traffic and Block policies), index, description, metadata (origin SYSTEM_DEFINED/USER_DEFINED/DERIVED, configurable on system policies). action is {type: ALLOW|BLOCK, allowReturnTraffic on ALLOW}. The protocol lives in ipProtocolScope.protocolFilter (NAMED_PROTOCOL or PRESET); ports and addresses live in source/destination.trafficFilter, which is omitted when a side matches the whole zone. Unset optional fields (ipsecFilter, schedule, connectionStateFilter) are omitted, not null. Evaluation order within a zone pair comes from unifi_get_firewall_policy_ordering.",
|
|
91
146
|
inputSchema: {
|
|
92
|
-
siteId:
|
|
147
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
93
148
|
offset: zod_1.z
|
|
94
149
|
.number()
|
|
95
150
|
.int()
|
|
96
151
|
.nonnegative()
|
|
97
152
|
.optional()
|
|
98
|
-
.describe("Number of records to skip
|
|
153
|
+
.describe("Number of records to skip"),
|
|
99
154
|
limit: zod_1.z
|
|
100
155
|
.number()
|
|
101
156
|
.int()
|
|
102
157
|
.min(0)
|
|
103
158
|
.max(200)
|
|
104
159
|
.optional()
|
|
105
|
-
.describe("Number of records to return (default: 25
|
|
160
|
+
.describe("Number of records to return (API default: 25)"),
|
|
106
161
|
filter: zod_1.z
|
|
107
162
|
.string()
|
|
108
163
|
.optional()
|
|
@@ -123,8 +178,8 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
123
178
|
server.registerTool("unifi_get_firewall_policy", {
|
|
124
179
|
description: "Get a firewall policy by ID with full match criteria and action. Returns the same fields as a unifi_list_firewall_policies entry, including loggingEnabled and reportFlowEnabled.",
|
|
125
180
|
inputSchema: {
|
|
126
|
-
siteId:
|
|
127
|
-
firewallPolicyId:
|
|
181
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
182
|
+
firewallPolicyId: (0, params_js_1.resourceId)().describe("Firewall policy ID"),
|
|
128
183
|
},
|
|
129
184
|
outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
|
|
130
185
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -140,12 +195,10 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
140
195
|
server.registerTool("unifi_get_firewall_policy_ordering", {
|
|
141
196
|
description: "Get the evaluation order of user-defined firewall policies for a specific (source zone, destination zone) pair. Returns: orderedFirewallPolicyIds.beforeSystemDefined[] and orderedFirewallPolicyIds.afterSystemDefined[] arrays of policy IDs. System-defined rules sit between these two arrays.",
|
|
142
197
|
inputSchema: {
|
|
143
|
-
siteId:
|
|
144
|
-
sourceFirewallZoneId:
|
|
145
|
-
.string()
|
|
198
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
199
|
+
sourceFirewallZoneId: (0, params_js_1.resourceId)()
|
|
146
200
|
.describe("Source firewall zone ID"),
|
|
147
|
-
destinationFirewallZoneId:
|
|
148
|
-
.string()
|
|
201
|
+
destinationFirewallZoneId: (0, params_js_1.resourceId)()
|
|
149
202
|
.describe("Destination firewall zone ID"),
|
|
150
203
|
},
|
|
151
204
|
outputSchema: output_schemas_js_1.firewallPolicyOrderingOutputSchema,
|
|
@@ -165,7 +218,7 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
165
218
|
server.registerTool("unifi_create_firewall_zone", {
|
|
166
219
|
description: "Create a new custom firewall zone",
|
|
167
220
|
inputSchema: {
|
|
168
|
-
siteId:
|
|
221
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
169
222
|
name: zod_1.z.string().describe("Zone name"),
|
|
170
223
|
networkIds: zod_1.z
|
|
171
224
|
.array(zod_1.z.string())
|
|
@@ -175,11 +228,14 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
175
228
|
.optional()
|
|
176
229
|
.describe("Preview this action without executing it"),
|
|
177
230
|
},
|
|
178
|
-
outputSchema: output_schemas_js_1.firewallZoneOutputSchema,
|
|
231
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.firewallZoneOutputSchema),
|
|
179
232
|
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
180
233
|
}, async ({ siteId, name, networkIds, dryRun }) => {
|
|
181
234
|
try {
|
|
182
235
|
const body = { name, networkIds };
|
|
236
|
+
const exposure = await new wan_exposure_js_1.WanExposure(client, wan, siteId).firewallZone(undefined, networkIds);
|
|
237
|
+
if (exposure)
|
|
238
|
+
return (0, wan_exposure_js_1.wanRefusal)(exposure, "POST", `/sites/${siteId}/firewall/zones`);
|
|
183
239
|
if (dryRun)
|
|
184
240
|
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/firewall/zones`, body);
|
|
185
241
|
const data = await client.post(`/sites/${siteId}/firewall/zones`, body);
|
|
@@ -190,10 +246,10 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
190
246
|
}
|
|
191
247
|
});
|
|
192
248
|
server.registerTool("unifi_update_firewall_zone", {
|
|
193
|
-
description: "
|
|
249
|
+
description: "Replace a firewall zone's name and networkIds (full PUT: both are required, and networkIds replaces the zone's whole network list, so start from unifi_get_firewall_zone).",
|
|
194
250
|
inputSchema: {
|
|
195
|
-
siteId:
|
|
196
|
-
firewallZoneId:
|
|
251
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
252
|
+
firewallZoneId: (0, params_js_1.resourceId)().describe("Firewall zone ID"),
|
|
197
253
|
name: zod_1.z.string().describe("Zone name"),
|
|
198
254
|
networkIds: zod_1.z
|
|
199
255
|
.array(zod_1.z.string())
|
|
@@ -203,11 +259,14 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
203
259
|
.optional()
|
|
204
260
|
.describe("Preview this action without executing it"),
|
|
205
261
|
},
|
|
206
|
-
outputSchema: output_schemas_js_1.firewallZoneOutputSchema,
|
|
262
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.firewallZoneOutputSchema),
|
|
207
263
|
annotations: safety_js_1.WRITE,
|
|
208
264
|
}, async ({ siteId, firewallZoneId, name, networkIds, dryRun }) => {
|
|
209
265
|
try {
|
|
210
266
|
const body = { name, networkIds };
|
|
267
|
+
const exposure = await new wan_exposure_js_1.WanExposure(client, wan, siteId).firewallZone(firewallZoneId, networkIds);
|
|
268
|
+
if (exposure)
|
|
269
|
+
return (0, wan_exposure_js_1.wanRefusal)(exposure, "PUT", `/sites/${siteId}/firewall/zones/${firewallZoneId}`);
|
|
211
270
|
if (dryRun)
|
|
212
271
|
return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/firewall/zones/${firewallZoneId}`, body);
|
|
213
272
|
const data = await client.put(`/sites/${siteId}/firewall/zones/${firewallZoneId}`, body);
|
|
@@ -220,12 +279,9 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
220
279
|
server.registerTool("unifi_delete_firewall_zone", {
|
|
221
280
|
description: "DESTRUCTIVE: Delete a custom firewall zone",
|
|
222
281
|
inputSchema: {
|
|
223
|
-
siteId:
|
|
224
|
-
firewallZoneId:
|
|
225
|
-
confirm:
|
|
226
|
-
.boolean()
|
|
227
|
-
.optional()
|
|
228
|
-
.describe("Must be true to execute this destructive action"),
|
|
282
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
283
|
+
firewallZoneId: (0, params_js_1.resourceId)().describe("Firewall zone ID"),
|
|
284
|
+
confirm: (0, safety_js_1.confirmDestructive)(),
|
|
229
285
|
dryRun: zod_1.z
|
|
230
286
|
.boolean()
|
|
231
287
|
.optional()
|
|
@@ -233,13 +289,17 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
233
289
|
},
|
|
234
290
|
annotations: safety_js_1.DESTRUCTIVE,
|
|
235
291
|
}, async ({ siteId, firewallZoneId, confirm, dryRun }) => {
|
|
236
|
-
const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will delete the firewall zone");
|
|
237
|
-
if (guard)
|
|
238
|
-
return guard;
|
|
239
292
|
try {
|
|
293
|
+
const path = `/sites/${siteId}/firewall/zones/${firewallZoneId}`;
|
|
294
|
+
const exposure = await new wan_exposure_js_1.WanExposure(client, wan, siteId).zoneDelete(firewallZoneId);
|
|
295
|
+
if (exposure)
|
|
296
|
+
return (0, wan_exposure_js_1.wanRefusal)(exposure, "DELETE", path);
|
|
297
|
+
const guard = dryRun ? null : (0, safety_js_1.requireConfirmation)(confirm, "This will delete the firewall zone");
|
|
298
|
+
if (guard)
|
|
299
|
+
return guard;
|
|
240
300
|
if (dryRun)
|
|
241
|
-
return (0, safety_js_1.formatDryRun)("DELETE",
|
|
242
|
-
const data = await client.delete(
|
|
301
|
+
return (0, safety_js_1.formatDryRun)("DELETE", path);
|
|
302
|
+
const data = await client.delete(path);
|
|
243
303
|
return (0, responses_js_1.formatSuccess)(data);
|
|
244
304
|
}
|
|
245
305
|
catch (err) {
|
|
@@ -249,21 +309,29 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
249
309
|
server.registerTool("unifi_create_firewall_policy", {
|
|
250
310
|
description: `Create a new firewall policy. Body fields (per 11.0.83 docs): ${FIREWALL_POLICY_BODY_DOC} Copy nested shapes (trafficFilter, protocolFilter) from an existing policy via unifi_get_firewall_policy. Idempotency: not safe to retry — re-running creates a duplicate policy.`,
|
|
251
311
|
inputSchema: {
|
|
252
|
-
siteId:
|
|
312
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
253
313
|
policy: zod_1.z
|
|
254
314
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
255
315
|
.describe("Firewall policy configuration (JSON object)"),
|
|
316
|
+
confirm: confirmOpening,
|
|
256
317
|
dryRun: zod_1.z
|
|
257
318
|
.boolean()
|
|
258
319
|
.optional()
|
|
259
320
|
.describe("Preview this action without executing it"),
|
|
260
321
|
},
|
|
261
|
-
outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
|
|
322
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.firewallPolicyOutputSchema),
|
|
262
323
|
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
263
|
-
}, async ({ siteId, policy, dryRun }) => {
|
|
324
|
+
}, async ({ siteId, policy, confirm, dryRun }) => {
|
|
264
325
|
try {
|
|
326
|
+
const exposure = await new wan_exposure_js_1.WanExposure(client, wan, siteId).firewallPolicy(policy);
|
|
327
|
+
if (exposure)
|
|
328
|
+
return (0, wan_exposure_js_1.wanRefusal)(exposure, "POST", `/sites/${siteId}/firewall/policies`);
|
|
329
|
+
const opening = firewallOpening(undefined, policy);
|
|
330
|
+
const guard = (0, safety_js_1.requireConfirmationIf)(opening, confirm, dryRun);
|
|
331
|
+
if (guard)
|
|
332
|
+
return guard;
|
|
265
333
|
if (dryRun)
|
|
266
|
-
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/firewall/policies`, policy);
|
|
334
|
+
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/firewall/policies`, policy, opening);
|
|
267
335
|
const data = await client.post(`/sites/${siteId}/firewall/policies`, policy);
|
|
268
336
|
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
269
337
|
}
|
|
@@ -274,19 +342,20 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
274
342
|
server.registerTool("unifi_update_firewall_policy", {
|
|
275
343
|
description: `Update a firewall policy. Pass only the fields to change in policy. The API's PUT replaces the whole policy — and an omitted reportFlowEnabled reverts to true — so this tool reads the current policy, keeps every documented body field, overlays your fields and sends the full body. Top-level fields are replaced whole (e.g. send the complete source object). Read-only response fields (id, index, metadata) are ignored. Body fields: ${FIREWALL_POLICY_BODY_DOC} For just logging/flow-reporting toggles, prefer unifi_patch_firewall_policy. dryRun shows the merged body.`,
|
|
276
344
|
inputSchema: {
|
|
277
|
-
siteId:
|
|
278
|
-
firewallPolicyId:
|
|
345
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
346
|
+
firewallPolicyId: (0, params_js_1.resourceId)().describe("Firewall policy ID"),
|
|
279
347
|
policy: zod_1.z
|
|
280
348
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
281
349
|
.describe("Firewall policy fields to change (JSON object); null clears an optional field such as schedule"),
|
|
350
|
+
confirm: confirmOpening,
|
|
282
351
|
dryRun: zod_1.z
|
|
283
352
|
.boolean()
|
|
284
353
|
.optional()
|
|
285
354
|
.describe("Preview the merged PUT body without executing it (still reads the current policy)"),
|
|
286
355
|
},
|
|
287
|
-
outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
|
|
356
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.firewallPolicyOutputSchema),
|
|
288
357
|
annotations: safety_js_1.WRITE,
|
|
289
|
-
}, async ({ siteId, firewallPolicyId, policy, dryRun }) => {
|
|
358
|
+
}, async ({ siteId, firewallPolicyId, policy, confirm, dryRun }) => {
|
|
290
359
|
try {
|
|
291
360
|
const path = `/sites/${siteId}/firewall/policies/${firewallPolicyId}`;
|
|
292
361
|
const updates = Object.fromEntries(Object.entries(policy).filter(([field]) => !FIREWALL_POLICY_READ_ONLY.includes(field)));
|
|
@@ -294,16 +363,25 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
294
363
|
if (rejected.length > 0) {
|
|
295
364
|
throw new Error(`Fields not accepted by the firewall policy PUT body: ${rejected.join(", ")}`);
|
|
296
365
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
366
|
+
return await (0, full_put_js_1.withResourceLock)(path, async () => {
|
|
367
|
+
const current = (0, full_put_js_1.asObject)(await client.get(path), "the current firewall policy");
|
|
368
|
+
const body = (0, full_put_js_1.mergeForPut)(current, updates, FIREWALL_POLICY_FIELDS);
|
|
369
|
+
const missing = (0, full_put_js_1.missingFields)(body, FIREWALL_POLICY_REQUIRED);
|
|
370
|
+
if (missing.length > 0) {
|
|
371
|
+
throw new Error(`Missing required fields: ${missing.join(", ")}`);
|
|
372
|
+
}
|
|
373
|
+
const exposure = await new wan_exposure_js_1.WanExposure(client, wan, siteId).firewallPolicy(body);
|
|
374
|
+
if (exposure)
|
|
375
|
+
return (0, wan_exposure_js_1.wanRefusal)(exposure, "PUT", path);
|
|
376
|
+
const opening = firewallOpening(current, body);
|
|
377
|
+
const guard = (0, safety_js_1.requireConfirmationIf)(opening, confirm, dryRun);
|
|
378
|
+
if (guard)
|
|
379
|
+
return guard;
|
|
380
|
+
if (dryRun)
|
|
381
|
+
return (0, safety_js_1.formatDryRun)("PUT", path, body, opening);
|
|
382
|
+
const data = await client.put(path, body);
|
|
383
|
+
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
384
|
+
});
|
|
307
385
|
}
|
|
308
386
|
catch (err) {
|
|
309
387
|
return (0, responses_js_1.formatError)(err);
|
|
@@ -312,24 +390,53 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
312
390
|
server.registerTool("unifi_patch_firewall_policy", {
|
|
313
391
|
description: "Partially update a firewall policy without resending all fields. loggingEnabled and reportFlowEnabled are the only fields documented as patchable in API 11.0.83 (reportFlowEnabled is ignored for the predefined Allow Return Traffic policies, which are never reported); other fields may be rejected — use unifi_update_firewall_policy for a full update. Idempotent for fields supplied.",
|
|
314
392
|
inputSchema: {
|
|
315
|
-
siteId:
|
|
316
|
-
firewallPolicyId:
|
|
393
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
394
|
+
firewallPolicyId: (0, params_js_1.resourceId)().describe("Firewall policy ID"),
|
|
317
395
|
policy: zod_1.z
|
|
318
396
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
319
397
|
.describe("Partial firewall policy fields to update (e.g. { loggingEnabled: true } or { reportFlowEnabled: false })"),
|
|
398
|
+
confirm: zod_1.z
|
|
399
|
+
.boolean()
|
|
400
|
+
.optional()
|
|
401
|
+
.describe("Set true to confirm a patch that can let traffic through, judged on the current policy with the patch applied: an ALLOW action, enabling an ALLOW policy, disabling a policy that blocks, or changing what an enabled policy matches. Logging and flow-reporting toggles never need it. A patch that could let traffic in from the WAN is refused, and confirm does not override that. Not needed with dryRun: true"),
|
|
320
402
|
dryRun: zod_1.z
|
|
321
403
|
.boolean()
|
|
322
404
|
.optional()
|
|
323
405
|
.describe("Preview this action without executing it"),
|
|
324
406
|
},
|
|
325
|
-
outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
|
|
407
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.firewallPolicyOutputSchema),
|
|
326
408
|
annotations: safety_js_1.WRITE,
|
|
327
|
-
}, async ({ siteId, firewallPolicyId, policy, dryRun }) => {
|
|
409
|
+
}, async ({ siteId, firewallPolicyId, policy, confirm, dryRun }) => {
|
|
328
410
|
try {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
return (0,
|
|
411
|
+
const path = `/sites/${siteId}/firewall/policies/${firewallPolicyId}`;
|
|
412
|
+
// Queue behind any in-flight update of this policy, whose PUT would
|
|
413
|
+
// otherwise overwrite this change with the values it read earlier.
|
|
414
|
+
return await (0, full_put_js_1.withResourceLock)(path, async () => {
|
|
415
|
+
// Toggling logging or flow reporting changes what is recorded, never
|
|
416
|
+
// what passes. Anything else is judged on the policy as the PATCH
|
|
417
|
+
// would leave it, which takes reading it first.
|
|
418
|
+
const reportingOnly = Object.keys(policy).every((field) => REPORTING_FIELDS.includes(field));
|
|
419
|
+
let opening;
|
|
420
|
+
if (!reportingOnly) {
|
|
421
|
+
const current = (0, full_put_js_1.asObject)(await client.get(path), "the current firewall policy");
|
|
422
|
+
const after = { ...current, ...policy };
|
|
423
|
+
// Disabling cannot let traffic in from the WAN.
|
|
424
|
+
const disablesOnly = Object.entries(policy).every(([field, value]) => REPORTING_FIELDS.includes(field) || (field === "enabled" && value === false));
|
|
425
|
+
if (!wan.allowExposure && !disablesOnly) {
|
|
426
|
+
const exposure = await new wan_exposure_js_1.WanExposure(client, wan, siteId).firewallPolicy(after);
|
|
427
|
+
if (exposure)
|
|
428
|
+
return (0, wan_exposure_js_1.wanRefusal)(exposure, "PATCH", path);
|
|
429
|
+
}
|
|
430
|
+
opening = firewallOpening(current, after);
|
|
431
|
+
const guard = (0, safety_js_1.requireConfirmationIf)(opening, confirm, dryRun);
|
|
432
|
+
if (guard)
|
|
433
|
+
return guard;
|
|
434
|
+
}
|
|
435
|
+
if (dryRun)
|
|
436
|
+
return (0, safety_js_1.formatDryRun)("PATCH", path, policy, opening);
|
|
437
|
+
const data = await client.patch(path, policy);
|
|
438
|
+
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
439
|
+
});
|
|
333
440
|
}
|
|
334
441
|
catch (err) {
|
|
335
442
|
return (0, responses_js_1.formatError)(err);
|
|
@@ -338,12 +445,9 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
338
445
|
server.registerTool("unifi_delete_firewall_policy", {
|
|
339
446
|
description: "DESTRUCTIVE: Delete a firewall policy",
|
|
340
447
|
inputSchema: {
|
|
341
|
-
siteId:
|
|
342
|
-
firewallPolicyId:
|
|
343
|
-
confirm:
|
|
344
|
-
.boolean()
|
|
345
|
-
.optional()
|
|
346
|
-
.describe("Must be true to execute this destructive action"),
|
|
448
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
449
|
+
firewallPolicyId: (0, params_js_1.resourceId)().describe("Firewall policy ID"),
|
|
450
|
+
confirm: (0, safety_js_1.confirmDestructive)(),
|
|
347
451
|
dryRun: zod_1.z
|
|
348
452
|
.boolean()
|
|
349
453
|
.optional()
|
|
@@ -351,12 +455,12 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
351
455
|
},
|
|
352
456
|
annotations: safety_js_1.DESTRUCTIVE,
|
|
353
457
|
}, async ({ siteId, firewallPolicyId, confirm, dryRun }) => {
|
|
354
|
-
const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will delete the firewall policy");
|
|
458
|
+
const guard = dryRun ? null : (0, safety_js_1.requireConfirmation)(confirm, "This will delete the firewall policy");
|
|
355
459
|
if (guard)
|
|
356
460
|
return guard;
|
|
357
461
|
try {
|
|
358
462
|
if (dryRun)
|
|
359
|
-
return (0, safety_js_1.formatDryRun)("DELETE", `/sites/${siteId}/firewall/policies/${firewallPolicyId}
|
|
463
|
+
return (0, safety_js_1.formatDryRun)("DELETE", `/sites/${siteId}/firewall/policies/${firewallPolicyId}`);
|
|
360
464
|
const data = await client.delete(`/sites/${siteId}/firewall/policies/${firewallPolicyId}`);
|
|
361
465
|
return (0, responses_js_1.formatSuccess)(data);
|
|
362
466
|
}
|
|
@@ -365,29 +469,50 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
365
469
|
}
|
|
366
470
|
});
|
|
367
471
|
server.registerTool("unifi_reorder_firewall_policies", {
|
|
368
|
-
description: "Reorder user-defined firewall policies for a zone pair",
|
|
472
|
+
description: "Reorder user-defined firewall policies for a zone pair. Policies are evaluated in order, user-defined ones in beforeSystemDefined ahead of the system-defined policies and those in afterSystemDefined after them. Moving a policy that lets traffic through ahead of one that blocks it, or across the system-defined policies, needs confirm. Reads the current order and the policies it moves past one another, on a dry run too.",
|
|
369
473
|
inputSchema: {
|
|
370
|
-
siteId:
|
|
371
|
-
sourceFirewallZoneId:
|
|
372
|
-
destinationFirewallZoneId:
|
|
373
|
-
.string()
|
|
474
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
475
|
+
sourceFirewallZoneId: (0, params_js_1.resourceId)().describe("Source firewall zone ID"),
|
|
476
|
+
destinationFirewallZoneId: (0, params_js_1.resourceId)()
|
|
374
477
|
.describe("Destination firewall zone ID"),
|
|
375
478
|
orderedFirewallPolicyIds: zod_1.z
|
|
376
479
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
377
480
|
.describe("Ordered policy IDs object with beforeSystemDefined and afterSystemDefined arrays"),
|
|
481
|
+
confirm: zod_1.z
|
|
482
|
+
.boolean()
|
|
483
|
+
.optional()
|
|
484
|
+
.describe("Set true to confirm a reorder that moves a policy that lets traffic through ahead of one that blocks it, or across the system-defined policies; not needed with dryRun: true"),
|
|
378
485
|
dryRun: zod_1.z
|
|
379
486
|
.boolean()
|
|
380
487
|
.optional()
|
|
381
488
|
.describe("Preview this action without executing it"),
|
|
382
489
|
},
|
|
383
490
|
annotations: safety_js_1.WRITE,
|
|
384
|
-
}, async ({ siteId, sourceFirewallZoneId, destinationFirewallZoneId, orderedFirewallPolicyIds, dryRun, }) => {
|
|
491
|
+
}, async ({ siteId, sourceFirewallZoneId, destinationFirewallZoneId, orderedFirewallPolicyIds, confirm, dryRun, }) => {
|
|
385
492
|
try {
|
|
493
|
+
const exposure = await new wan_exposure_js_1.WanExposure(client, wan, siteId).policyOrder(sourceFirewallZoneId);
|
|
494
|
+
if (exposure)
|
|
495
|
+
return (0, wan_exposure_js_1.wanRefusal)(exposure, "PUT", `/sites/${siteId}/firewall/policies/ordering`);
|
|
386
496
|
const query = `?sourceFirewallZoneId=${encodeURIComponent(sourceFirewallZoneId)}&destinationFirewallZoneId=${encodeURIComponent(destinationFirewallZoneId)}`;
|
|
497
|
+
const path = `/sites/${siteId}/firewall/policies/ordering${query}`;
|
|
387
498
|
const body = { orderedFirewallPolicyIds };
|
|
499
|
+
const current = (0, full_put_js_1.asObject)(await client.get(path), "the current firewall policy ordering");
|
|
500
|
+
const opens = await (0, safety_js_1.reorderOpens)(evaluationOrder(current.orderedFirewallPolicyIds), evaluationOrder(orderedFirewallPolicyIds), async (id) => {
|
|
501
|
+
// The system-defined policies are not listed, so their actions are
|
|
502
|
+
// unknown: moving anything across them counts both ways.
|
|
503
|
+
if (id === SYSTEM_DEFINED)
|
|
504
|
+
return { letsThrough: true, blocks: true };
|
|
505
|
+
const policy = (0, full_put_js_1.asObject)(await client.get(`/sites/${siteId}/firewall/policies/${id}`), "a firewall policy");
|
|
506
|
+
const active = policy.enabled !== false;
|
|
507
|
+
return { letsThrough: active && letsThrough(policy), blocks: active && !letsThrough(policy) };
|
|
508
|
+
});
|
|
509
|
+
const opening = opens ? "This moves a firewall policy that lets traffic through ahead of one that blocks it" : undefined;
|
|
510
|
+
const guard = (0, safety_js_1.requireConfirmationIf)(opening, confirm, dryRun);
|
|
511
|
+
if (guard)
|
|
512
|
+
return guard;
|
|
388
513
|
if (dryRun)
|
|
389
|
-
return (0, safety_js_1.formatDryRun)("PUT",
|
|
390
|
-
const data = await client.put(
|
|
514
|
+
return (0, safety_js_1.formatDryRun)("PUT", path, body, opening);
|
|
515
|
+
const data = await client.put(path, body);
|
|
391
516
|
return (0, responses_js_1.formatSuccess)(data);
|
|
392
517
|
}
|
|
393
518
|
catch (err) {
|