iflow-mcp_enuno-unifi-mcp-server 0.2.1__py3-none-any.whl
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.
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/METADATA +1282 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/RECORD +81 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/WHEEL +4 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/entry_points.txt +2 -0
- iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/licenses/LICENSE +201 -0
- src/__init__.py +3 -0
- src/__main__.py +6 -0
- src/api/__init__.py +5 -0
- src/api/client.py +727 -0
- src/api/site_manager_client.py +176 -0
- src/cache.py +483 -0
- src/config/__init__.py +5 -0
- src/config/config.py +321 -0
- src/main.py +2234 -0
- src/models/__init__.py +126 -0
- src/models/acl.py +41 -0
- src/models/backup.py +272 -0
- src/models/client.py +74 -0
- src/models/device.py +53 -0
- src/models/dpi.py +50 -0
- src/models/firewall_policy.py +123 -0
- src/models/firewall_zone.py +28 -0
- src/models/network.py +62 -0
- src/models/qos_profile.py +458 -0
- src/models/radius.py +141 -0
- src/models/reference_data.py +34 -0
- src/models/site.py +59 -0
- src/models/site_manager.py +120 -0
- src/models/topology.py +138 -0
- src/models/traffic_flow.py +137 -0
- src/models/traffic_matching_list.py +56 -0
- src/models/voucher.py +42 -0
- src/models/vpn.py +73 -0
- src/models/wan.py +48 -0
- src/models/zbf_matrix.py +49 -0
- src/resources/__init__.py +8 -0
- src/resources/clients.py +111 -0
- src/resources/devices.py +102 -0
- src/resources/networks.py +93 -0
- src/resources/site_manager.py +64 -0
- src/resources/sites.py +86 -0
- src/tools/__init__.py +25 -0
- src/tools/acls.py +328 -0
- src/tools/application.py +42 -0
- src/tools/backups.py +1173 -0
- src/tools/client_management.py +505 -0
- src/tools/clients.py +203 -0
- src/tools/device_control.py +325 -0
- src/tools/devices.py +354 -0
- src/tools/dpi.py +241 -0
- src/tools/dpi_tools.py +89 -0
- src/tools/firewall.py +417 -0
- src/tools/firewall_policies.py +430 -0
- src/tools/firewall_zones.py +515 -0
- src/tools/network_config.py +388 -0
- src/tools/networks.py +190 -0
- src/tools/port_forwarding.py +263 -0
- src/tools/qos.py +1070 -0
- src/tools/radius.py +763 -0
- src/tools/reference_data.py +107 -0
- src/tools/site_manager.py +466 -0
- src/tools/site_vpn.py +95 -0
- src/tools/sites.py +187 -0
- src/tools/topology.py +406 -0
- src/tools/traffic_flows.py +1062 -0
- src/tools/traffic_matching_lists.py +371 -0
- src/tools/vouchers.py +249 -0
- src/tools/vpn.py +76 -0
- src/tools/wans.py +30 -0
- src/tools/wifi.py +498 -0
- src/tools/zbf_matrix.py +326 -0
- src/utils/__init__.py +88 -0
- src/utils/audit.py +213 -0
- src/utils/exceptions.py +114 -0
- src/utils/helpers.py +159 -0
- src/utils/logger.py +105 -0
- src/utils/sanitize.py +244 -0
- src/utils/validators.py +160 -0
- src/webhooks/__init__.py +6 -0
- src/webhooks/handlers.py +196 -0
- src/webhooks/receiver.py +290 -0
src/tools/zbf_matrix.py
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Zone-Based Firewall matrix management tools.
|
|
2
|
+
|
|
3
|
+
⚠️ IMPORTANT: All tools in this file are DEPRECATED as of 2025-11-18.
|
|
4
|
+
|
|
5
|
+
Endpoint verification on UniFi Express 7 and UDM Pro (API v10.0.156) confirmed
|
|
6
|
+
that the zone policy matrix and application blocking endpoints DO NOT EXIST.
|
|
7
|
+
|
|
8
|
+
The following endpoints were tested and returned 404:
|
|
9
|
+
- /sites/{siteId}/firewall/policies/zone-matrix
|
|
10
|
+
- /sites/{siteId}/firewall/policies/zones/{zoneId}
|
|
11
|
+
- /sites/{siteId}/firewall/zones/{zoneId}/policies
|
|
12
|
+
- /sites/{siteId}/firewall/zones/{zoneId}/applications/block
|
|
13
|
+
- /sites/{siteId}/firewall/zones/{zoneId}/applications/blocked
|
|
14
|
+
|
|
15
|
+
Workarounds:
|
|
16
|
+
- Configure zone policies manually in UniFi Console UI
|
|
17
|
+
- Use traditional ACL rules (/sites/{siteId}/acls) for IP-based filtering
|
|
18
|
+
- Use DPI categories for application blocking at network level
|
|
19
|
+
|
|
20
|
+
See tests/verification/PHASE2_FINDINGS.md for complete verification report.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from ..api.client import UniFiClient # noqa: F401
|
|
26
|
+
from ..config import Settings
|
|
27
|
+
from ..models.zbf_matrix import ApplicationBlockRule, ZonePolicy, ZonePolicyMatrix # noqa: F401
|
|
28
|
+
from ..utils import audit_action, get_logger, validate_confirmation # noqa: F401
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def get_zbf_matrix(site_id: str, settings: Settings) -> dict[str, Any]:
|
|
34
|
+
"""Retrieve zone-to-zone policy matrix.
|
|
35
|
+
|
|
36
|
+
⚠️ **DEPRECATED - ENDPOINT DOES NOT EXIST**
|
|
37
|
+
|
|
38
|
+
This endpoint has been verified to NOT EXIST in UniFi Network API v10.0.156.
|
|
39
|
+
Tested on UniFi Express 7 and UDM Pro on 2025-11-18.
|
|
40
|
+
|
|
41
|
+
The zone policy matrix must be configured via the UniFi Console UI.
|
|
42
|
+
Use traditional ACL rules (/sites/{siteId}/acls) as a workaround.
|
|
43
|
+
|
|
44
|
+
See tests/verification/PHASE2_FINDINGS.md for details.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
site_id: Site identifier
|
|
48
|
+
settings: Application settings
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Zone policy matrix with all zones and policies
|
|
52
|
+
|
|
53
|
+
Raises:
|
|
54
|
+
NotImplementedError: This endpoint does not exist in the UniFi API
|
|
55
|
+
"""
|
|
56
|
+
logger.warning(
|
|
57
|
+
"get_zbf_matrix called but endpoint does not exist in UniFi API v10.0.156. "
|
|
58
|
+
"Configure zone policies via UniFi Console UI instead."
|
|
59
|
+
)
|
|
60
|
+
raise NotImplementedError(
|
|
61
|
+
"Zone policy matrix endpoint does not exist in UniFi Network API v10.0.156. "
|
|
62
|
+
"Verified on U7 Express and UDM Pro (2025-11-18). "
|
|
63
|
+
"Configure zone policies manually in UniFi Console. "
|
|
64
|
+
"See tests/verification/PHASE2_FINDINGS.md for details."
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
async def get_zone_policies(site_id: str, zone_id: str, settings: Settings) -> list[dict[str, Any]]:
|
|
69
|
+
"""Get policies for a specific zone.
|
|
70
|
+
|
|
71
|
+
⚠️ **DEPRECATED - ENDPOINT DOES NOT EXIST**
|
|
72
|
+
|
|
73
|
+
This endpoint has been verified to NOT EXIST in UniFi Network API v10.0.156.
|
|
74
|
+
Tested on UniFi Express 7 and UDM Pro on 2025-11-18.
|
|
75
|
+
|
|
76
|
+
Zone policies must be configured via the UniFi Console UI.
|
|
77
|
+
|
|
78
|
+
See tests/verification/PHASE2_FINDINGS.md for details.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
site_id: Site identifier
|
|
82
|
+
zone_id: Zone identifier
|
|
83
|
+
settings: Application settings
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
List of policies for the zone
|
|
87
|
+
|
|
88
|
+
Raises:
|
|
89
|
+
NotImplementedError: This endpoint does not exist in the UniFi API
|
|
90
|
+
"""
|
|
91
|
+
logger.warning(
|
|
92
|
+
f"get_zone_policies called for zone {zone_id} but endpoint does not exist in UniFi API v10.0.156."
|
|
93
|
+
)
|
|
94
|
+
raise NotImplementedError(
|
|
95
|
+
"Zone policies endpoint does not exist in UniFi Network API v10.0.156. "
|
|
96
|
+
"Verified on U7 Express and UDM Pro (2025-11-18). "
|
|
97
|
+
"Configure zone policies manually in UniFi Console. "
|
|
98
|
+
"See tests/verification/PHASE2_FINDINGS.md for details."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
async def update_zbf_policy(
|
|
103
|
+
site_id: str,
|
|
104
|
+
source_zone_id: str,
|
|
105
|
+
destination_zone_id: str,
|
|
106
|
+
action: str,
|
|
107
|
+
settings: Settings,
|
|
108
|
+
description: str | None = None,
|
|
109
|
+
priority: int | None = None,
|
|
110
|
+
enabled: bool = True,
|
|
111
|
+
confirm: bool = False,
|
|
112
|
+
dry_run: bool = False,
|
|
113
|
+
) -> dict[str, Any]:
|
|
114
|
+
"""Modify inter-zone firewall policy.
|
|
115
|
+
|
|
116
|
+
⚠️ **DEPRECATED - ENDPOINT DOES NOT EXIST**
|
|
117
|
+
|
|
118
|
+
This endpoint has been verified to NOT EXIST in UniFi Network API v10.0.156.
|
|
119
|
+
Tested on UniFi Express 7 and UDM Pro on 2025-11-18.
|
|
120
|
+
|
|
121
|
+
Zone-to-zone policies must be configured via the UniFi Console UI.
|
|
122
|
+
|
|
123
|
+
See tests/verification/PHASE2_FINDINGS.md for details.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
site_id: Site identifier
|
|
127
|
+
source_zone_id: Source zone identifier
|
|
128
|
+
destination_zone_id: Destination zone identifier
|
|
129
|
+
action: Policy action (allow/deny)
|
|
130
|
+
settings: Application settings
|
|
131
|
+
description: Policy description
|
|
132
|
+
priority: Policy priority
|
|
133
|
+
enabled: Whether policy is enabled
|
|
134
|
+
confirm: Confirmation flag (required)
|
|
135
|
+
dry_run: If True, validate but don't execute
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Updated policy
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
NotImplementedError: This endpoint does not exist in the UniFi API
|
|
142
|
+
"""
|
|
143
|
+
logger.warning(
|
|
144
|
+
f"update_zbf_policy called for {source_zone_id} -> {destination_zone_id} "
|
|
145
|
+
"but endpoint does not exist in UniFi API v10.0.156."
|
|
146
|
+
)
|
|
147
|
+
raise NotImplementedError(
|
|
148
|
+
"Zone policy update endpoint does not exist in UniFi Network API v10.0.156. "
|
|
149
|
+
"Verified on U7 Express and UDM Pro (2025-11-18). "
|
|
150
|
+
"Configure zone policies manually in UniFi Console. "
|
|
151
|
+
"See tests/verification/PHASE2_FINDINGS.md for details."
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def block_application_by_zone(
|
|
156
|
+
site_id: str,
|
|
157
|
+
zone_id: str,
|
|
158
|
+
application_id: str,
|
|
159
|
+
settings: Settings,
|
|
160
|
+
action: str = "block",
|
|
161
|
+
enabled: bool = True,
|
|
162
|
+
description: str | None = None,
|
|
163
|
+
confirm: bool = False,
|
|
164
|
+
dry_run: bool = False,
|
|
165
|
+
) -> dict[str, Any]:
|
|
166
|
+
"""Block applications using zone-based rules.
|
|
167
|
+
|
|
168
|
+
⚠️ **DEPRECATED - ENDPOINT DOES NOT EXIST**
|
|
169
|
+
|
|
170
|
+
This endpoint has been verified to NOT EXIST in UniFi Network API v10.0.156.
|
|
171
|
+
Tested on UniFi Express 7 and UDM Pro on 2025-11-18.
|
|
172
|
+
|
|
173
|
+
Application blocking per zone is not available via the API.
|
|
174
|
+
Use DPI categories for application blocking at the network level instead.
|
|
175
|
+
|
|
176
|
+
See tests/verification/PHASE2_FINDINGS.md for details.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
site_id: Site identifier
|
|
180
|
+
zone_id: Zone identifier
|
|
181
|
+
application_id: DPI application identifier
|
|
182
|
+
settings: Application settings
|
|
183
|
+
action: Action to take (block/allow)
|
|
184
|
+
enabled: Whether rule is enabled
|
|
185
|
+
description: Rule description
|
|
186
|
+
confirm: Confirmation flag (required)
|
|
187
|
+
dry_run: If True, validate but don't execute
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Created application block rule
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
NotImplementedError: This endpoint does not exist in the UniFi API
|
|
194
|
+
"""
|
|
195
|
+
logger.warning(
|
|
196
|
+
f"block_application_by_zone called for zone {zone_id}, app {application_id} "
|
|
197
|
+
"but endpoint does not exist in UniFi API v10.0.156."
|
|
198
|
+
)
|
|
199
|
+
raise NotImplementedError(
|
|
200
|
+
"Application blocking per zone endpoint does not exist in UniFi Network API v10.0.156. "
|
|
201
|
+
"Verified on U7 Express and UDM Pro (2025-11-18). "
|
|
202
|
+
"Use DPI categories for application blocking at network level. "
|
|
203
|
+
"See tests/verification/PHASE2_FINDINGS.md for details."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def list_blocked_applications(
|
|
208
|
+
site_id: str, zone_id: str | None = None, settings: Settings | None = None
|
|
209
|
+
) -> list[dict[str, Any]]:
|
|
210
|
+
"""List applications blocked per zone.
|
|
211
|
+
|
|
212
|
+
⚠️ **DEPRECATED - ENDPOINT DOES NOT EXIST**
|
|
213
|
+
|
|
214
|
+
This endpoint has been verified to NOT EXIST in UniFi Network API v10.0.156.
|
|
215
|
+
Tested on UniFi Express 7 and UDM Pro on 2025-11-18.
|
|
216
|
+
|
|
217
|
+
Application blocking per zone is not available via the API.
|
|
218
|
+
|
|
219
|
+
See tests/verification/PHASE2_FINDINGS.md for details.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
site_id: Site identifier
|
|
223
|
+
zone_id: Optional zone identifier to filter by
|
|
224
|
+
settings: Application settings
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
List of blocked applications
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
NotImplementedError: This endpoint does not exist in the UniFi API
|
|
231
|
+
"""
|
|
232
|
+
logger.warning(
|
|
233
|
+
f"list_blocked_applications called for site {site_id} "
|
|
234
|
+
"but endpoint does not exist in UniFi API v10.0.156."
|
|
235
|
+
)
|
|
236
|
+
raise NotImplementedError(
|
|
237
|
+
"Blocked applications list endpoint does not exist in UniFi Network API v10.0.156. "
|
|
238
|
+
"Verified on U7 Express and UDM Pro (2025-11-18). "
|
|
239
|
+
"See tests/verification/PHASE2_FINDINGS.md for details."
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
async def get_zone_matrix_policy(
|
|
244
|
+
site_id: str,
|
|
245
|
+
source_zone_id: str,
|
|
246
|
+
destination_zone_id: str,
|
|
247
|
+
settings: Settings,
|
|
248
|
+
) -> dict[str, Any]:
|
|
249
|
+
"""Get a specific zone-to-zone policy.
|
|
250
|
+
|
|
251
|
+
⚠️ **DEPRECATED - ENDPOINT DOES NOT EXIST**
|
|
252
|
+
|
|
253
|
+
This endpoint has been verified to NOT EXIST in UniFi Network API v10.0.156.
|
|
254
|
+
Tested on UniFi Express 7 and UDM Pro on 2025-11-18.
|
|
255
|
+
|
|
256
|
+
Zone-to-zone policies must be configured via the UniFi Console UI.
|
|
257
|
+
|
|
258
|
+
See tests/verification/PHASE2_FINDINGS.md for details.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
site_id: Site identifier
|
|
262
|
+
source_zone_id: Source zone identifier
|
|
263
|
+
destination_zone_id: Destination zone identifier
|
|
264
|
+
settings: Application settings
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Zone-to-zone policy details
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
NotImplementedError: This endpoint does not exist in the UniFi API
|
|
271
|
+
"""
|
|
272
|
+
logger.warning(
|
|
273
|
+
f"get_zone_matrix_policy called for {source_zone_id} -> {destination_zone_id} "
|
|
274
|
+
"but endpoint does not exist in UniFi API v10.0.156."
|
|
275
|
+
)
|
|
276
|
+
raise NotImplementedError(
|
|
277
|
+
"Zone matrix policy endpoint does not exist in UniFi Network API v10.0.156. "
|
|
278
|
+
"Verified on U7 Express and UDM Pro (2025-11-18). "
|
|
279
|
+
"Configure zone policies manually in UniFi Console. "
|
|
280
|
+
"See tests/verification/PHASE2_FINDINGS.md for details."
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
async def delete_zbf_policy(
|
|
285
|
+
site_id: str,
|
|
286
|
+
source_zone_id: str,
|
|
287
|
+
destination_zone_id: str,
|
|
288
|
+
settings: Settings,
|
|
289
|
+
confirm: bool = False,
|
|
290
|
+
dry_run: bool = False,
|
|
291
|
+
) -> dict[str, Any]:
|
|
292
|
+
"""Delete a zone-to-zone policy (revert to default action).
|
|
293
|
+
|
|
294
|
+
⚠️ **DEPRECATED - ENDPOINT DOES NOT EXIST**
|
|
295
|
+
|
|
296
|
+
This endpoint has been verified to NOT EXIST in UniFi Network API v10.0.156.
|
|
297
|
+
Tested on UniFi Express 7 and UDM Pro on 2025-11-18.
|
|
298
|
+
|
|
299
|
+
Zone-to-zone policies must be configured via the UniFi Console UI.
|
|
300
|
+
|
|
301
|
+
See tests/verification/PHASE2_FINDINGS.md for details.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
site_id: Site identifier
|
|
305
|
+
source_zone_id: Source zone identifier
|
|
306
|
+
destination_zone_id: Destination zone identifier
|
|
307
|
+
settings: Application settings
|
|
308
|
+
confirm: Confirmation flag (required)
|
|
309
|
+
dry_run: If True, validate but don't execute
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Deletion confirmation
|
|
313
|
+
|
|
314
|
+
Raises:
|
|
315
|
+
NotImplementedError: This endpoint does not exist in the UniFi API
|
|
316
|
+
"""
|
|
317
|
+
logger.warning(
|
|
318
|
+
f"delete_zbf_policy called for {source_zone_id} -> {destination_zone_id} "
|
|
319
|
+
"but endpoint does not exist in UniFi API v10.0.156."
|
|
320
|
+
)
|
|
321
|
+
raise NotImplementedError(
|
|
322
|
+
"Zone policy delete endpoint does not exist in UniFi Network API v10.0.156. "
|
|
323
|
+
"Verified on U7 Express and UDM Pro (2025-11-18). "
|
|
324
|
+
"Configure zone policies manually in UniFi Console. "
|
|
325
|
+
"See tests/verification/PHASE2_FINDINGS.md for details."
|
|
326
|
+
)
|
src/utils/__init__.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Utility modules for UniFi MCP Server."""
|
|
2
|
+
|
|
3
|
+
from .audit import AuditLogger, audit_action, get_audit_logger, log_audit
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
APIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
ConfigurationError,
|
|
8
|
+
ConfirmationRequiredError,
|
|
9
|
+
NetworkError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ResourceNotFoundError,
|
|
12
|
+
UniFiMCPException,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
from .helpers import (
|
|
16
|
+
build_uri,
|
|
17
|
+
format_bytes,
|
|
18
|
+
format_percentage,
|
|
19
|
+
format_uptime,
|
|
20
|
+
get_iso_timestamp,
|
|
21
|
+
get_timestamp,
|
|
22
|
+
merge_dicts,
|
|
23
|
+
parse_device_type,
|
|
24
|
+
sanitize_dict,
|
|
25
|
+
)
|
|
26
|
+
from .logger import get_logger, log_api_request, log_audit_event
|
|
27
|
+
from .sanitize import sanitize_dict as sanitize_sensitive_dict
|
|
28
|
+
from .sanitize import (
|
|
29
|
+
sanitize_for_logging,
|
|
30
|
+
sanitize_list,
|
|
31
|
+
sanitize_log_message,
|
|
32
|
+
sanitize_sensitive_data,
|
|
33
|
+
)
|
|
34
|
+
from .validators import (
|
|
35
|
+
validate_confirmation,
|
|
36
|
+
validate_device_id,
|
|
37
|
+
validate_ip_address,
|
|
38
|
+
validate_limit_offset,
|
|
39
|
+
validate_mac_address,
|
|
40
|
+
validate_port,
|
|
41
|
+
validate_site_id,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
__all__ = [
|
|
45
|
+
# Exceptions
|
|
46
|
+
"UniFiMCPException",
|
|
47
|
+
"ConfigurationError",
|
|
48
|
+
"AuthenticationError",
|
|
49
|
+
"APIError",
|
|
50
|
+
"RateLimitError",
|
|
51
|
+
"ResourceNotFoundError",
|
|
52
|
+
"ValidationError",
|
|
53
|
+
"NetworkError",
|
|
54
|
+
"ConfirmationRequiredError",
|
|
55
|
+
# Audit
|
|
56
|
+
"AuditLogger",
|
|
57
|
+
"get_audit_logger",
|
|
58
|
+
"log_audit",
|
|
59
|
+
"audit_action",
|
|
60
|
+
# Logger
|
|
61
|
+
"get_logger",
|
|
62
|
+
"log_api_request",
|
|
63
|
+
"log_audit_event",
|
|
64
|
+
# Sanitization
|
|
65
|
+
"sanitize_sensitive_dict",
|
|
66
|
+
"sanitize_for_logging",
|
|
67
|
+
"sanitize_list",
|
|
68
|
+
"sanitize_log_message",
|
|
69
|
+
"sanitize_sensitive_data",
|
|
70
|
+
# Validators
|
|
71
|
+
"validate_mac_address",
|
|
72
|
+
"validate_ip_address",
|
|
73
|
+
"validate_port",
|
|
74
|
+
"validate_site_id",
|
|
75
|
+
"validate_device_id",
|
|
76
|
+
"validate_confirmation",
|
|
77
|
+
"validate_limit_offset",
|
|
78
|
+
# Helpers
|
|
79
|
+
"get_timestamp",
|
|
80
|
+
"get_iso_timestamp",
|
|
81
|
+
"format_uptime",
|
|
82
|
+
"format_bytes",
|
|
83
|
+
"format_percentage",
|
|
84
|
+
"sanitize_dict",
|
|
85
|
+
"merge_dicts",
|
|
86
|
+
"parse_device_type",
|
|
87
|
+
"build_uri",
|
|
88
|
+
]
|
src/utils/audit.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"""Audit logging for mutating operations."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .helpers import get_iso_timestamp
|
|
8
|
+
from .logger import get_logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AuditLogger:
|
|
12
|
+
"""Audit logger for tracking mutating operations."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, log_file: str | Path | None = None, log_level: str = "INFO"):
|
|
15
|
+
"""Initialize audit logger.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
log_file: Path to audit log file. If None, uses default location.
|
|
19
|
+
log_level: Logging level
|
|
20
|
+
"""
|
|
21
|
+
self.log_file = Path(log_file) if log_file else Path("audit.log")
|
|
22
|
+
self.logger = get_logger(__name__, log_level)
|
|
23
|
+
|
|
24
|
+
# Ensure log directory exists
|
|
25
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
26
|
+
|
|
27
|
+
def log_operation(
|
|
28
|
+
self,
|
|
29
|
+
operation: str,
|
|
30
|
+
parameters: dict[str, Any],
|
|
31
|
+
result: str,
|
|
32
|
+
user: str | None = None,
|
|
33
|
+
site_id: str | None = None,
|
|
34
|
+
dry_run: bool = False,
|
|
35
|
+
error: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Log a mutating operation.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
operation: Name of the operation (e.g., "create_firewall_rule")
|
|
41
|
+
parameters: Parameters passed to the operation
|
|
42
|
+
result: Result of the operation ("success", "failed", "dry_run")
|
|
43
|
+
user: User who performed the operation (optional)
|
|
44
|
+
error: Error message if the operation failed (optional)
|
|
45
|
+
site_id: Site ID where operation was performed
|
|
46
|
+
dry_run: Whether this was a dry run
|
|
47
|
+
"""
|
|
48
|
+
timestamp = get_iso_timestamp()
|
|
49
|
+
|
|
50
|
+
# Create audit record
|
|
51
|
+
audit_record = {
|
|
52
|
+
"timestamp": timestamp,
|
|
53
|
+
"operation": operation,
|
|
54
|
+
"parameters": parameters,
|
|
55
|
+
"result": result,
|
|
56
|
+
"dry_run": dry_run,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if user:
|
|
60
|
+
audit_record["user"] = user
|
|
61
|
+
|
|
62
|
+
if site_id:
|
|
63
|
+
audit_record["site_id"] = site_id
|
|
64
|
+
|
|
65
|
+
if error:
|
|
66
|
+
audit_record["error"] = error
|
|
67
|
+
|
|
68
|
+
# Log to file
|
|
69
|
+
try:
|
|
70
|
+
with open(self.log_file, "a", encoding="utf-8") as f:
|
|
71
|
+
f.write(json.dumps(audit_record) + "\n")
|
|
72
|
+
except Exception as e:
|
|
73
|
+
self.logger.error(f"Failed to write audit log: {e}")
|
|
74
|
+
|
|
75
|
+
# Log to application logger
|
|
76
|
+
log_message = f"AUDIT: {operation} - {result}"
|
|
77
|
+
if dry_run:
|
|
78
|
+
log_message += " (DRY RUN)"
|
|
79
|
+
|
|
80
|
+
if result == "success":
|
|
81
|
+
self.logger.info(log_message, extra=audit_record)
|
|
82
|
+
elif result == "failed":
|
|
83
|
+
self.logger.warning(log_message, extra=audit_record)
|
|
84
|
+
else:
|
|
85
|
+
self.logger.info(log_message, extra=audit_record)
|
|
86
|
+
|
|
87
|
+
def get_recent_operations(
|
|
88
|
+
self, limit: int = 100, operation: str | None = None
|
|
89
|
+
) -> list[dict[str, Any]]:
|
|
90
|
+
"""Get recent audit log entries.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
limit: Maximum number of entries to return
|
|
94
|
+
operation: Filter by operation name (optional)
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
List of audit log entries
|
|
98
|
+
"""
|
|
99
|
+
if not self.log_file.exists():
|
|
100
|
+
return []
|
|
101
|
+
|
|
102
|
+
entries = []
|
|
103
|
+
try:
|
|
104
|
+
with open(self.log_file, encoding="utf-8") as f:
|
|
105
|
+
# Read file in reverse to get most recent entries first
|
|
106
|
+
lines = f.readlines()
|
|
107
|
+
for line in reversed(lines):
|
|
108
|
+
if not line.strip():
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
entry = json.loads(line)
|
|
113
|
+
if operation is None or entry.get("operation") == operation:
|
|
114
|
+
entries.append(entry)
|
|
115
|
+
|
|
116
|
+
if len(entries) >= limit:
|
|
117
|
+
break
|
|
118
|
+
except json.JSONDecodeError:
|
|
119
|
+
self.logger.warning(f"Invalid JSON in audit log: {line}")
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
except Exception as e:
|
|
123
|
+
self.logger.error(f"Failed to read audit log: {e}")
|
|
124
|
+
|
|
125
|
+
return entries
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# Global audit logger instance
|
|
129
|
+
_audit_logger: AuditLogger | None = None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def get_audit_logger(log_file: str | Path | None = None, log_level: str = "INFO") -> AuditLogger:
|
|
133
|
+
"""Get or create the global audit logger instance.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
log_file: Path to audit log file
|
|
137
|
+
log_level: Logging level
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
AuditLogger instance
|
|
141
|
+
"""
|
|
142
|
+
global _audit_logger
|
|
143
|
+
|
|
144
|
+
if _audit_logger is None:
|
|
145
|
+
_audit_logger = AuditLogger(log_file, log_level)
|
|
146
|
+
|
|
147
|
+
return _audit_logger
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def log_audit(
|
|
151
|
+
operation: str,
|
|
152
|
+
parameters: dict[str, Any],
|
|
153
|
+
result: str,
|
|
154
|
+
user: str | None = None,
|
|
155
|
+
site_id: str | None = None,
|
|
156
|
+
dry_run: bool = False,
|
|
157
|
+
error: str | None = None,
|
|
158
|
+
log_file: str | Path | None = None,
|
|
159
|
+
) -> None:
|
|
160
|
+
"""Convenience function to log an audit entry.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
operation: Name of the operation
|
|
164
|
+
parameters: Parameters passed to the operation
|
|
165
|
+
result: Result of the operation
|
|
166
|
+
user: User who performed the operation
|
|
167
|
+
site_id: Site ID where operation was performed
|
|
168
|
+
dry_run: Whether this was a dry run
|
|
169
|
+
error: Error message if the operation failed
|
|
170
|
+
log_file: Path to audit log file
|
|
171
|
+
"""
|
|
172
|
+
logger = get_audit_logger(log_file)
|
|
173
|
+
logger.log_operation(operation, parameters, result, user, site_id, dry_run, error)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
async def audit_action(
|
|
177
|
+
settings: Any,
|
|
178
|
+
action_type: str,
|
|
179
|
+
resource_type: str,
|
|
180
|
+
resource_id: str,
|
|
181
|
+
site_id: str,
|
|
182
|
+
details: dict[str, Any] | None = None,
|
|
183
|
+
) -> None:
|
|
184
|
+
"""Audit a mutating action.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
settings: Application settings
|
|
188
|
+
action_type: Type of action (e.g., "create_firewall_zone")
|
|
189
|
+
resource_type: Type of resource (e.g., "firewall_zone")
|
|
190
|
+
resource_id: Resource identifier
|
|
191
|
+
site_id: Site identifier
|
|
192
|
+
details: Additional details about the action
|
|
193
|
+
"""
|
|
194
|
+
parameters = {
|
|
195
|
+
"action_type": action_type,
|
|
196
|
+
"resource_type": resource_type,
|
|
197
|
+
"resource_id": resource_id,
|
|
198
|
+
"site_id": site_id,
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if details:
|
|
202
|
+
parameters["details"] = details # type: ignore[assignment]
|
|
203
|
+
|
|
204
|
+
# Get audit log file from settings if available
|
|
205
|
+
log_file = getattr(settings, "audit_log_file", None)
|
|
206
|
+
|
|
207
|
+
log_audit(
|
|
208
|
+
operation=action_type,
|
|
209
|
+
parameters=parameters,
|
|
210
|
+
result="success",
|
|
211
|
+
site_id=site_id,
|
|
212
|
+
log_file=log_file,
|
|
213
|
+
)
|