@owine/unifi-network-mcp 2.14.2 → 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 +235 -80
- 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 +126 -53
- package/dist/tools/client-history.js +20 -10
- package/dist/tools/clients.js +14 -13
- package/dist/tools/devices.js +30 -29
- package/dist/tools/dns-policies.js +116 -69
- package/dist/tools/firewall.d.ts +2 -1
- package/dist/tools/firewall.js +245 -80
- 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 +152 -58
- package/dist/tools/sites.js +3 -3
- package/dist/tools/supporting.js +31 -30
- package/dist/tools/switching.js +22 -21
- package/dist/tools/system.js +1 -1
- package/dist/tools/traffic-matching.d.ts +2 -1
- package/dist/tools/traffic-matching.js +31 -28
- package/dist/tools/wifi.js +228 -311
- package/dist/utils/full-put.d.ts +32 -0
- package/dist/utils/full-put.js +74 -0
- package/dist/utils/output-schemas.d.ts +77 -9
- package/dist/utils/output-schemas.js +106 -54
- 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.d.ts +24 -0
- package/dist/utils/secrets.js +92 -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 +19 -15
package/dist/tools/firewall.js
CHANGED
|
@@ -1,33 +1,116 @@
|
|
|
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");
|
|
9
|
+
const full_put_js_1 = require("../utils/full-put.js");
|
|
8
10
|
const output_schemas_js_1 = require("../utils/output-schemas.js");
|
|
9
|
-
|
|
11
|
+
const params_js_1 = require("../utils/params.js");
|
|
12
|
+
const wan_exposure_js_1 = require("../utils/wan-exposure.js");
|
|
13
|
+
// Firewall policy request-body fields, from the 11.0.83 POST/PUT schemas.
|
|
14
|
+
const FIREWALL_POLICY_FIELDS = [
|
|
15
|
+
"enabled",
|
|
16
|
+
"name",
|
|
17
|
+
"description",
|
|
18
|
+
"action",
|
|
19
|
+
"source",
|
|
20
|
+
"destination",
|
|
21
|
+
"ipProtocolScope",
|
|
22
|
+
"connectionStateFilter",
|
|
23
|
+
"ipsecFilter",
|
|
24
|
+
"loggingEnabled",
|
|
25
|
+
"reportFlowEnabled",
|
|
26
|
+
"schedule",
|
|
27
|
+
];
|
|
28
|
+
const FIREWALL_POLICY_REQUIRED = [
|
|
29
|
+
"enabled",
|
|
30
|
+
"name",
|
|
31
|
+
"action",
|
|
32
|
+
"source",
|
|
33
|
+
"destination",
|
|
34
|
+
"ipProtocolScope",
|
|
35
|
+
"loggingEnabled",
|
|
36
|
+
];
|
|
37
|
+
// PATCH fields that change what is logged or reported, never what passes.
|
|
38
|
+
const REPORTING_FIELDS = ["loggingEnabled", "reportFlowEnabled"];
|
|
39
|
+
// Present in responses but not part of the request body.
|
|
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");
|
|
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).";
|
|
92
|
+
function registerFirewallTools(server, client, readOnly = false, wan = wan_exposure_js_1.DEFAULT_WAN_POLICY) {
|
|
10
93
|
server.registerTool("unifi_list_firewall_zones", {
|
|
11
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.",
|
|
12
95
|
inputSchema: {
|
|
13
|
-
siteId:
|
|
96
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
14
97
|
offset: zod_1.z
|
|
15
98
|
.number()
|
|
16
99
|
.int()
|
|
17
100
|
.nonnegative()
|
|
18
101
|
.optional()
|
|
19
|
-
.describe("Number of records to skip
|
|
102
|
+
.describe("Number of records to skip"),
|
|
20
103
|
limit: zod_1.z
|
|
21
104
|
.number()
|
|
22
105
|
.int()
|
|
23
106
|
.min(0)
|
|
24
107
|
.max(200)
|
|
25
108
|
.optional()
|
|
26
|
-
.describe("Number of records to return (default: 25
|
|
109
|
+
.describe("Number of records to return (API default: 25)"),
|
|
27
110
|
filter: zod_1.z
|
|
28
111
|
.string()
|
|
29
112
|
.optional()
|
|
30
|
-
.describe("Filter expression"),
|
|
113
|
+
.describe("Filter expression, e.g. name.eq('Internal'). NOTE: totalCount ignores the filter on this endpoint (observed 11.0.83) — page by count"),
|
|
31
114
|
},
|
|
32
115
|
outputSchema: output_schemas_js_1.listFirewallZonesOutputSchema,
|
|
33
116
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -44,8 +127,8 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
44
127
|
server.registerTool("unifi_get_firewall_zone", {
|
|
45
128
|
description: "Get a firewall zone by ID (same fields as the list entry).",
|
|
46
129
|
inputSchema: {
|
|
47
|
-
siteId:
|
|
48
|
-
firewallZoneId:
|
|
130
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
131
|
+
firewallZoneId: (0, params_js_1.resourceId)().describe("Firewall zone ID"),
|
|
49
132
|
},
|
|
50
133
|
outputSchema: output_schemas_js_1.firewallZoneOutputSchema,
|
|
51
134
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -59,26 +142,26 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
59
142
|
}
|
|
60
143
|
});
|
|
61
144
|
server.registerTool("unifi_list_firewall_policies", {
|
|
62
|
-
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, index, description, metadata
|
|
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.",
|
|
63
146
|
inputSchema: {
|
|
64
|
-
siteId:
|
|
147
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
65
148
|
offset: zod_1.z
|
|
66
149
|
.number()
|
|
67
150
|
.int()
|
|
68
151
|
.nonnegative()
|
|
69
152
|
.optional()
|
|
70
|
-
.describe("Number of records to skip
|
|
153
|
+
.describe("Number of records to skip"),
|
|
71
154
|
limit: zod_1.z
|
|
72
155
|
.number()
|
|
73
156
|
.int()
|
|
74
157
|
.min(0)
|
|
75
158
|
.max(200)
|
|
76
159
|
.optional()
|
|
77
|
-
.describe("Number of records to return (default: 25
|
|
160
|
+
.describe("Number of records to return (API default: 25)"),
|
|
78
161
|
filter: zod_1.z
|
|
79
162
|
.string()
|
|
80
163
|
.optional()
|
|
81
|
-
.describe("Filter expression"),
|
|
164
|
+
.describe("Filter expression, e.g. metadata.origin.eq('USER_DEFINED') or name.eq('Allow Return Traffic'). Boolean fields (enabled, loggingEnabled, reportFlowEnabled) are not filterable"),
|
|
82
165
|
},
|
|
83
166
|
outputSchema: output_schemas_js_1.listFirewallPoliciesOutputSchema,
|
|
84
167
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -93,10 +176,10 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
93
176
|
}
|
|
94
177
|
});
|
|
95
178
|
server.registerTool("unifi_get_firewall_policy", {
|
|
96
|
-
description: "Get a firewall policy by ID with full match criteria and action.",
|
|
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.",
|
|
97
180
|
inputSchema: {
|
|
98
|
-
siteId:
|
|
99
|
-
firewallPolicyId:
|
|
181
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
182
|
+
firewallPolicyId: (0, params_js_1.resourceId)().describe("Firewall policy ID"),
|
|
100
183
|
},
|
|
101
184
|
outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
|
|
102
185
|
annotations: safety_js_1.READ_ONLY,
|
|
@@ -110,14 +193,12 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
110
193
|
}
|
|
111
194
|
});
|
|
112
195
|
server.registerTool("unifi_get_firewall_policy_ordering", {
|
|
113
|
-
description: "Get the evaluation order of user-defined firewall policies for a specific (source zone, destination zone) pair. Returns: beforeSystemDefined[] and afterSystemDefined[] arrays of policy IDs. System-defined rules sit between these two arrays.",
|
|
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.",
|
|
114
197
|
inputSchema: {
|
|
115
|
-
siteId:
|
|
116
|
-
sourceFirewallZoneId:
|
|
117
|
-
.string()
|
|
198
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
199
|
+
sourceFirewallZoneId: (0, params_js_1.resourceId)()
|
|
118
200
|
.describe("Source firewall zone ID"),
|
|
119
|
-
destinationFirewallZoneId:
|
|
120
|
-
.string()
|
|
201
|
+
destinationFirewallZoneId: (0, params_js_1.resourceId)()
|
|
121
202
|
.describe("Destination firewall zone ID"),
|
|
122
203
|
},
|
|
123
204
|
outputSchema: output_schemas_js_1.firewallPolicyOrderingOutputSchema,
|
|
@@ -137,7 +218,7 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
137
218
|
server.registerTool("unifi_create_firewall_zone", {
|
|
138
219
|
description: "Create a new custom firewall zone",
|
|
139
220
|
inputSchema: {
|
|
140
|
-
siteId:
|
|
221
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
141
222
|
name: zod_1.z.string().describe("Zone name"),
|
|
142
223
|
networkIds: zod_1.z
|
|
143
224
|
.array(zod_1.z.string())
|
|
@@ -147,11 +228,14 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
147
228
|
.optional()
|
|
148
229
|
.describe("Preview this action without executing it"),
|
|
149
230
|
},
|
|
150
|
-
outputSchema: output_schemas_js_1.firewallZoneOutputSchema,
|
|
231
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.firewallZoneOutputSchema),
|
|
151
232
|
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
152
233
|
}, async ({ siteId, name, networkIds, dryRun }) => {
|
|
153
234
|
try {
|
|
154
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`);
|
|
155
239
|
if (dryRun)
|
|
156
240
|
return (0, safety_js_1.formatDryRun)("POST", `/sites/${siteId}/firewall/zones`, body);
|
|
157
241
|
const data = await client.post(`/sites/${siteId}/firewall/zones`, body);
|
|
@@ -162,10 +246,10 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
162
246
|
}
|
|
163
247
|
});
|
|
164
248
|
server.registerTool("unifi_update_firewall_zone", {
|
|
165
|
-
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).",
|
|
166
250
|
inputSchema: {
|
|
167
|
-
siteId:
|
|
168
|
-
firewallZoneId:
|
|
251
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
252
|
+
firewallZoneId: (0, params_js_1.resourceId)().describe("Firewall zone ID"),
|
|
169
253
|
name: zod_1.z.string().describe("Zone name"),
|
|
170
254
|
networkIds: zod_1.z
|
|
171
255
|
.array(zod_1.z.string())
|
|
@@ -175,11 +259,14 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
175
259
|
.optional()
|
|
176
260
|
.describe("Preview this action without executing it"),
|
|
177
261
|
},
|
|
178
|
-
outputSchema: output_schemas_js_1.firewallZoneOutputSchema,
|
|
262
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.firewallZoneOutputSchema),
|
|
179
263
|
annotations: safety_js_1.WRITE,
|
|
180
264
|
}, async ({ siteId, firewallZoneId, name, networkIds, dryRun }) => {
|
|
181
265
|
try {
|
|
182
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}`);
|
|
183
270
|
if (dryRun)
|
|
184
271
|
return (0, safety_js_1.formatDryRun)("PUT", `/sites/${siteId}/firewall/zones/${firewallZoneId}`, body);
|
|
185
272
|
const data = await client.put(`/sites/${siteId}/firewall/zones/${firewallZoneId}`, body);
|
|
@@ -192,12 +279,9 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
192
279
|
server.registerTool("unifi_delete_firewall_zone", {
|
|
193
280
|
description: "DESTRUCTIVE: Delete a custom firewall zone",
|
|
194
281
|
inputSchema: {
|
|
195
|
-
siteId:
|
|
196
|
-
firewallZoneId:
|
|
197
|
-
confirm:
|
|
198
|
-
.boolean()
|
|
199
|
-
.optional()
|
|
200
|
-
.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)(),
|
|
201
285
|
dryRun: zod_1.z
|
|
202
286
|
.boolean()
|
|
203
287
|
.optional()
|
|
@@ -205,13 +289,17 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
205
289
|
},
|
|
206
290
|
annotations: safety_js_1.DESTRUCTIVE,
|
|
207
291
|
}, async ({ siteId, firewallZoneId, confirm, dryRun }) => {
|
|
208
|
-
const guard = (0, safety_js_1.requireConfirmation)(confirm, "This will delete the firewall zone");
|
|
209
|
-
if (guard)
|
|
210
|
-
return guard;
|
|
211
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;
|
|
212
300
|
if (dryRun)
|
|
213
|
-
return (0, safety_js_1.formatDryRun)("DELETE",
|
|
214
|
-
const data = await client.delete(
|
|
301
|
+
return (0, safety_js_1.formatDryRun)("DELETE", path);
|
|
302
|
+
const data = await client.delete(path);
|
|
215
303
|
return (0, responses_js_1.formatSuccess)(data);
|
|
216
304
|
}
|
|
217
305
|
catch (err) {
|
|
@@ -219,23 +307,31 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
219
307
|
}
|
|
220
308
|
});
|
|
221
309
|
server.registerTool("unifi_create_firewall_policy", {
|
|
222
|
-
description:
|
|
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.`,
|
|
223
311
|
inputSchema: {
|
|
224
|
-
siteId:
|
|
312
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
225
313
|
policy: zod_1.z
|
|
226
314
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
227
315
|
.describe("Firewall policy configuration (JSON object)"),
|
|
316
|
+
confirm: confirmOpening,
|
|
228
317
|
dryRun: zod_1.z
|
|
229
318
|
.boolean()
|
|
230
319
|
.optional()
|
|
231
320
|
.describe("Preview this action without executing it"),
|
|
232
321
|
},
|
|
233
|
-
outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
|
|
322
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.firewallPolicyOutputSchema),
|
|
234
323
|
annotations: safety_js_1.WRITE_NOT_IDEMPOTENT,
|
|
235
|
-
}, async ({ siteId, policy, dryRun }) => {
|
|
324
|
+
}, async ({ siteId, policy, confirm, dryRun }) => {
|
|
236
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;
|
|
237
333
|
if (dryRun)
|
|
238
|
-
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);
|
|
239
335
|
const data = await client.post(`/sites/${siteId}/firewall/policies`, policy);
|
|
240
336
|
return (0, responses_js_1.formatSuccess)(data, { structured: true });
|
|
241
337
|
}
|
|
@@ -244,52 +340,103 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
244
340
|
}
|
|
245
341
|
});
|
|
246
342
|
server.registerTool("unifi_update_firewall_policy", {
|
|
247
|
-
description:
|
|
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.`,
|
|
248
344
|
inputSchema: {
|
|
249
|
-
siteId:
|
|
250
|
-
firewallPolicyId:
|
|
345
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
346
|
+
firewallPolicyId: (0, params_js_1.resourceId)().describe("Firewall policy ID"),
|
|
251
347
|
policy: zod_1.z
|
|
252
348
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
253
|
-
.describe("Firewall policy
|
|
349
|
+
.describe("Firewall policy fields to change (JSON object); null clears an optional field such as schedule"),
|
|
350
|
+
confirm: confirmOpening,
|
|
254
351
|
dryRun: zod_1.z
|
|
255
352
|
.boolean()
|
|
256
353
|
.optional()
|
|
257
|
-
.describe("Preview
|
|
354
|
+
.describe("Preview the merged PUT body without executing it (still reads the current policy)"),
|
|
258
355
|
},
|
|
259
|
-
outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
|
|
356
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.firewallPolicyOutputSchema),
|
|
260
357
|
annotations: safety_js_1.WRITE,
|
|
261
|
-
}, async ({ siteId, firewallPolicyId, policy, dryRun }) => {
|
|
358
|
+
}, async ({ siteId, firewallPolicyId, policy, confirm, dryRun }) => {
|
|
262
359
|
try {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
|
|
360
|
+
const path = `/sites/${siteId}/firewall/policies/${firewallPolicyId}`;
|
|
361
|
+
const updates = Object.fromEntries(Object.entries(policy).filter(([field]) => !FIREWALL_POLICY_READ_ONLY.includes(field)));
|
|
362
|
+
const rejected = (0, full_put_js_1.unsupportedFields)(updates, FIREWALL_POLICY_FIELDS);
|
|
363
|
+
if (rejected.length > 0) {
|
|
364
|
+
throw new Error(`Fields not accepted by the firewall policy PUT body: ${rejected.join(", ")}`);
|
|
365
|
+
}
|
|
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
|
+
});
|
|
267
385
|
}
|
|
268
386
|
catch (err) {
|
|
269
387
|
return (0, responses_js_1.formatError)(err);
|
|
270
388
|
}
|
|
271
389
|
});
|
|
272
390
|
server.registerTool("unifi_patch_firewall_policy", {
|
|
273
|
-
description: "Partially update a firewall policy without resending all fields. loggingEnabled
|
|
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.",
|
|
274
392
|
inputSchema: {
|
|
275
|
-
siteId:
|
|
276
|
-
firewallPolicyId:
|
|
393
|
+
siteId: (0, params_js_1.resourceId)().describe("Site ID"),
|
|
394
|
+
firewallPolicyId: (0, params_js_1.resourceId)().describe("Firewall policy ID"),
|
|
277
395
|
policy: zod_1.z
|
|
278
396
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
279
|
-
.describe("Partial firewall policy fields to update (e.g. { loggingEnabled: true })"),
|
|
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"),
|
|
280
402
|
dryRun: zod_1.z
|
|
281
403
|
.boolean()
|
|
282
404
|
.optional()
|
|
283
405
|
.describe("Preview this action without executing it"),
|
|
284
406
|
},
|
|
285
|
-
outputSchema: output_schemas_js_1.firewallPolicyOutputSchema,
|
|
407
|
+
outputSchema: (0, output_schemas_js_1.writeResult)(output_schemas_js_1.firewallPolicyOutputSchema),
|
|
286
408
|
annotations: safety_js_1.WRITE,
|
|
287
|
-
}, async ({ siteId, firewallPolicyId, policy, dryRun }) => {
|
|
409
|
+
}, async ({ siteId, firewallPolicyId, policy, confirm, dryRun }) => {
|
|
288
410
|
try {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
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
|
+
});
|
|
293
440
|
}
|
|
294
441
|
catch (err) {
|
|
295
442
|
return (0, responses_js_1.formatError)(err);
|
|
@@ -298,12 +445,9 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
298
445
|
server.registerTool("unifi_delete_firewall_policy", {
|
|
299
446
|
description: "DESTRUCTIVE: Delete a firewall policy",
|
|
300
447
|
inputSchema: {
|
|
301
|
-
siteId:
|
|
302
|
-
firewallPolicyId:
|
|
303
|
-
confirm:
|
|
304
|
-
.boolean()
|
|
305
|
-
.optional()
|
|
306
|
-
.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)(),
|
|
307
451
|
dryRun: zod_1.z
|
|
308
452
|
.boolean()
|
|
309
453
|
.optional()
|
|
@@ -311,12 +455,12 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
311
455
|
},
|
|
312
456
|
annotations: safety_js_1.DESTRUCTIVE,
|
|
313
457
|
}, async ({ siteId, firewallPolicyId, confirm, dryRun }) => {
|
|
314
|
-
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");
|
|
315
459
|
if (guard)
|
|
316
460
|
return guard;
|
|
317
461
|
try {
|
|
318
462
|
if (dryRun)
|
|
319
|
-
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}`);
|
|
320
464
|
const data = await client.delete(`/sites/${siteId}/firewall/policies/${firewallPolicyId}`);
|
|
321
465
|
return (0, responses_js_1.formatSuccess)(data);
|
|
322
466
|
}
|
|
@@ -325,29 +469,50 @@ function registerFirewallTools(server, client, readOnly = false) {
|
|
|
325
469
|
}
|
|
326
470
|
});
|
|
327
471
|
server.registerTool("unifi_reorder_firewall_policies", {
|
|
328
|
-
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.",
|
|
329
473
|
inputSchema: {
|
|
330
|
-
siteId:
|
|
331
|
-
sourceFirewallZoneId:
|
|
332
|
-
destinationFirewallZoneId:
|
|
333
|
-
.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)()
|
|
334
477
|
.describe("Destination firewall zone ID"),
|
|
335
478
|
orderedFirewallPolicyIds: zod_1.z
|
|
336
479
|
.record(zod_1.z.string(), zod_1.z.unknown())
|
|
337
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"),
|
|
338
485
|
dryRun: zod_1.z
|
|
339
486
|
.boolean()
|
|
340
487
|
.optional()
|
|
341
488
|
.describe("Preview this action without executing it"),
|
|
342
489
|
},
|
|
343
490
|
annotations: safety_js_1.WRITE,
|
|
344
|
-
}, async ({ siteId, sourceFirewallZoneId, destinationFirewallZoneId, orderedFirewallPolicyIds, dryRun, }) => {
|
|
491
|
+
}, async ({ siteId, sourceFirewallZoneId, destinationFirewallZoneId, orderedFirewallPolicyIds, confirm, dryRun, }) => {
|
|
345
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`);
|
|
346
496
|
const query = `?sourceFirewallZoneId=${encodeURIComponent(sourceFirewallZoneId)}&destinationFirewallZoneId=${encodeURIComponent(destinationFirewallZoneId)}`;
|
|
497
|
+
const path = `/sites/${siteId}/firewall/policies/ordering${query}`;
|
|
347
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;
|
|
348
513
|
if (dryRun)
|
|
349
|
-
return (0, safety_js_1.formatDryRun)("PUT",
|
|
350
|
-
const data = await client.put(
|
|
514
|
+
return (0, safety_js_1.formatDryRun)("PUT", path, body, opening);
|
|
515
|
+
const data = await client.put(path, body);
|
|
351
516
|
return (0, responses_js_1.formatSuccess)(data);
|
|
352
517
|
}
|
|
353
518
|
catch (err) {
|