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.
Files changed (81) hide show
  1. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/METADATA +1282 -0
  2. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/RECORD +81 -0
  3. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/WHEEL +4 -0
  4. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/entry_points.txt +2 -0
  5. iflow_mcp_enuno_unifi_mcp_server-0.2.1.dist-info/licenses/LICENSE +201 -0
  6. src/__init__.py +3 -0
  7. src/__main__.py +6 -0
  8. src/api/__init__.py +5 -0
  9. src/api/client.py +727 -0
  10. src/api/site_manager_client.py +176 -0
  11. src/cache.py +483 -0
  12. src/config/__init__.py +5 -0
  13. src/config/config.py +321 -0
  14. src/main.py +2234 -0
  15. src/models/__init__.py +126 -0
  16. src/models/acl.py +41 -0
  17. src/models/backup.py +272 -0
  18. src/models/client.py +74 -0
  19. src/models/device.py +53 -0
  20. src/models/dpi.py +50 -0
  21. src/models/firewall_policy.py +123 -0
  22. src/models/firewall_zone.py +28 -0
  23. src/models/network.py +62 -0
  24. src/models/qos_profile.py +458 -0
  25. src/models/radius.py +141 -0
  26. src/models/reference_data.py +34 -0
  27. src/models/site.py +59 -0
  28. src/models/site_manager.py +120 -0
  29. src/models/topology.py +138 -0
  30. src/models/traffic_flow.py +137 -0
  31. src/models/traffic_matching_list.py +56 -0
  32. src/models/voucher.py +42 -0
  33. src/models/vpn.py +73 -0
  34. src/models/wan.py +48 -0
  35. src/models/zbf_matrix.py +49 -0
  36. src/resources/__init__.py +8 -0
  37. src/resources/clients.py +111 -0
  38. src/resources/devices.py +102 -0
  39. src/resources/networks.py +93 -0
  40. src/resources/site_manager.py +64 -0
  41. src/resources/sites.py +86 -0
  42. src/tools/__init__.py +25 -0
  43. src/tools/acls.py +328 -0
  44. src/tools/application.py +42 -0
  45. src/tools/backups.py +1173 -0
  46. src/tools/client_management.py +505 -0
  47. src/tools/clients.py +203 -0
  48. src/tools/device_control.py +325 -0
  49. src/tools/devices.py +354 -0
  50. src/tools/dpi.py +241 -0
  51. src/tools/dpi_tools.py +89 -0
  52. src/tools/firewall.py +417 -0
  53. src/tools/firewall_policies.py +430 -0
  54. src/tools/firewall_zones.py +515 -0
  55. src/tools/network_config.py +388 -0
  56. src/tools/networks.py +190 -0
  57. src/tools/port_forwarding.py +263 -0
  58. src/tools/qos.py +1070 -0
  59. src/tools/radius.py +763 -0
  60. src/tools/reference_data.py +107 -0
  61. src/tools/site_manager.py +466 -0
  62. src/tools/site_vpn.py +95 -0
  63. src/tools/sites.py +187 -0
  64. src/tools/topology.py +406 -0
  65. src/tools/traffic_flows.py +1062 -0
  66. src/tools/traffic_matching_lists.py +371 -0
  67. src/tools/vouchers.py +249 -0
  68. src/tools/vpn.py +76 -0
  69. src/tools/wans.py +30 -0
  70. src/tools/wifi.py +498 -0
  71. src/tools/zbf_matrix.py +326 -0
  72. src/utils/__init__.py +88 -0
  73. src/utils/audit.py +213 -0
  74. src/utils/exceptions.py +114 -0
  75. src/utils/helpers.py +159 -0
  76. src/utils/logger.py +105 -0
  77. src/utils/sanitize.py +244 -0
  78. src/utils/validators.py +160 -0
  79. src/webhooks/__init__.py +6 -0
  80. src/webhooks/handlers.py +196 -0
  81. src/webhooks/receiver.py +290 -0
@@ -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
+ )