PyPANRestV2 2.1.0__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.
pypanrestv2/Objects.py ADDED
@@ -0,0 +1,1428 @@
1
+ from typing import Optional, Dict, Any, Tuple, Union, List, Protocol, Set, TypeVar
2
+ from . import ApplicationHelper
3
+ from . import Exceptions
4
+ import pycountry
5
+ import ipaddress
6
+ import builtins
7
+ import time
8
+ import re
9
+ from datetime import datetime
10
+ from icecream import ic
11
+ import sys
12
+ import xmltodict
13
+ import dns.resolver
14
+ import requests
15
+ from requests.packages.urllib3.exceptions import InsecureRequestWarning
16
+ import logging
17
+ import xml.etree.ElementTree as ET
18
+ from pypanrestv2.Base import Base, PAN, Panorama, Firewall
19
+ logger = logging.getLogger(__name__)
20
+ requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
21
+
22
+
23
+ class Object(Base, PAN):
24
+ """
25
+ Base class for all objects
26
+ """
27
+ valid_objects: List[str] = [
28
+ 'Addresses', 'AddressGroups', 'Regions', 'DynamicUserGroups', 'Applications',
29
+ 'ApplicationGroups', 'ApplicationFilters', 'Services', 'ServiceGroups', 'Tags',
30
+ 'ExternalDynamicLists', 'CustomURLCategories']
31
+ allowed_name_pattern = re.compile(r"[0-9a-zA-Z._-]+", re.IGNORECASE)
32
+
33
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
34
+ Base.__init__(self, PANDevice, **kwargs)
35
+ PAN.__init__(self, PANDevice.base_url, api_key=PANDevice.api_key)
36
+ self.endpoint: str = 'Objects'
37
+ self.disable_override: str = kwargs.get('disable-override', 'no')
38
+ self.has_tags: bool = kwargs.get('has_tags', False)
39
+ self.pan_objects: Dict[str, Any] = {}
40
+
41
+ @property
42
+ def disable_override(self) -> str:
43
+ return self._disable_override
44
+
45
+ @disable_override.setter
46
+ def disable_override(self, val: str) -> None:
47
+ # Normalize the value to lowercase if it's a string
48
+ val = val.lower() if isinstance(val, str) else val
49
+
50
+ # Validate the value
51
+ if val not in ['yes', 'no', None]:
52
+ raise ValueError(f"'disable_override' value must be 'yes', 'no', or None, not {val}.")
53
+
54
+ # For 'shared' location, 'disable_override' should be None
55
+ if self.location == 'shared':
56
+ self._disable_override = None
57
+ else:
58
+ # Default to 'no' if the value is None and location is not 'shared'
59
+ self._disable_override = val if val is not None else 'no'
60
+
61
+ # Update the entry dictionary if not in 'shared' location and if PANDevice is not a Firewall
62
+ if self.location != 'shared' and not isinstance(self.PANDevice, Firewall):
63
+ self.entry.update({'disable-override': self._disable_override})
64
+
65
+ def get(self, **kwargs) -> Union[bool, List]:
66
+ """
67
+ Get an "Object" item from the firewall or panorama.
68
+ ANYLOCATION: Bool: If true, search all valid locations for the object
69
+ IsSearch: Bool: Don't bother logging output if we are just seearching to see if it is there.
70
+ :return: Object or list of all objects is name is none
71
+ """
72
+ # Built in regions are not enumerable via the API
73
+ if pycountry.countries.get(alpha_2=self.name):
74
+ return [pycountry.countries.get(alpha_2=self.name)]
75
+
76
+ IsSearch = kwargs.get('IsSearch') or False
77
+ ANYLOCATION = kwargs.get('ANYLOCATION') or False
78
+ params = {'name': self.name,
79
+ 'location': self.location}
80
+ if isinstance(self.PANDevice, Panorama):
81
+ if self.location != 'shared':
82
+ params.update({'device-group': self.loc})
83
+ else:
84
+ params.update({'vsys': self.loc})
85
+
86
+ # We can't use the PAN base class method rest_request because we need the error codes from the device
87
+ # to determine how we should search for a named object.
88
+ url = f"{self.base_url}/restapi/{self.ver}/{self.endpoint}/{self.__class__.__name__}"
89
+ response = self.session.request('GET', url, params=params).json()
90
+
91
+ if response.get('code') == 3:
92
+ if response.get('message').startswith('Invalid Query Parameter: location'):
93
+ # the object could be in a different location
94
+ for location in self.valid_location:
95
+ params.update({
96
+ 'location': location,
97
+ })
98
+ response = self.rest_request('GET', params=params)
99
+ if response.get('@status') == 'success':
100
+ return response.get('result', {}).get('entry')
101
+ # Could not find object
102
+ if not IsSearch:
103
+ logger.error(
104
+ f'Could not find object {self.name} in any location on the device {self.PANDevice.hostname}')
105
+ return False
106
+ else:
107
+ if not IsSearch:
108
+ logger.error(f'Could not get object {self.name} from device {self.PANDevice.hostname}')
109
+ return False
110
+ if response.get('code') == 5:
111
+ if ANYLOCATION and self.location != 'shared':
112
+ if response.get('message') == 'Object Not Present':
113
+ # the object could be in a different location
114
+ if isinstance(self.PANDevice, Panorama):
115
+ # If this is a device group in Panorama, the object we are looking for can be in any
116
+ # parent device group within the device group hierarchy.
117
+ parent = self.PANDevice.device_groups_list.get(self.loc, {}).get('parent')
118
+ while True:
119
+ if parent == 'shared':
120
+ params.update({'location': parent})
121
+ if params.get('device-group'):
122
+ params.pop('device-group')
123
+ else:
124
+ params.update({'device-group': parent})
125
+ response = self.rest_request('GET', params=params)
126
+ if response.get('@status') == 'success':
127
+ logger.info(
128
+ f'Could not find {self.name} in location {self.loc}, however we did '
129
+ f'find it in {parent}.')
130
+ return response.get('result', {}).get('entry')
131
+ if parent == 'shared':
132
+ # There are no more parent device groups to search
133
+ if not IsSearch:
134
+ logger.warning(f'Could not find {self.name} in any device group on '
135
+ f'{self.PANDevice.IP}.')
136
+ break
137
+ else:
138
+ parent = self.PANDevice.device_groups_list.get(parent, {}).get('parent')
139
+ else:
140
+ for location in self.valid_location:
141
+ params.update({
142
+ 'location': location,
143
+ })
144
+ response = self.rest_request('GET', params=params)
145
+ if response.get('@status') == 'success':
146
+ logger.info(
147
+ f'Could not find {self.name} in location {self.loc}, however we did '
148
+ f'find it in {self.location} : {location or self.loc} ')
149
+ return response.get('result', {}).get('entry')
150
+ # Could not find object
151
+ if not IsSearch:
152
+ logger.error(
153
+ f'Could not find object {self.name} in any location on the device {self.PANDevice.hostname}')
154
+ return False
155
+ else:
156
+ if not IsSearch:
157
+ logger.warning(
158
+ f'The {self.__class__.__name__} object {self.name} not found on device {self.PANDevice.hostname}')
159
+ return False
160
+ if response.get('@status') == 'success':
161
+ return response.get('result', {}).get('entry')
162
+ else:
163
+ if not IsSearch:
164
+ logger.warning(f'Could not get object {self.name} from device {self.PANDevice.hostname}')
165
+ return False
166
+
167
+ def refresh(self) -> bool:
168
+ """
169
+ Retrieves live data from a device and updates the instance attributes based on the data.
170
+ Ensures that only one entry is returned from the data retrieval call and dynamically sets
171
+ the instance attributes based on the data keys, modifying them if necessary.
172
+
173
+ :return: True if the refresh is successful and instance attributes are updated, False otherwise.
174
+ """
175
+ if not self.name:
176
+ logger.error('The name attribute must be available to do a refresh.')
177
+ return False
178
+
179
+ entry: List[Dict[str, Any]] = self.get(ANYLOCATION=True, IsSearch=True)
180
+ if not entry:
181
+ return False
182
+
183
+ if len(entry) > 1:
184
+ error_message: str = 'More than one entry returned; cannot refresh.'
185
+ logger.error(error_message)
186
+ raise ValueError(error_message)
187
+
188
+ updated = False
189
+ for key, value in entry[0].items():
190
+ if key == '@name':
191
+ setattr(self, 'name', value)
192
+ updated = True
193
+ continue
194
+
195
+ # need to replace any - with _ so it can be used as an attribute
196
+ modified_key: str = key.replace('-', '_')
197
+ # Append an underscore if the key is a Python built-in name
198
+ if modified_key in dir(builtins):
199
+ modified_key += '_'
200
+
201
+ if modified_key.startswith('@'):
202
+ setattr(self, modified_key.lstrip('@'), value)
203
+ updated = True
204
+ continue
205
+ # Check if the attribute exists in the instance before setting it
206
+ if hasattr(self, modified_key):
207
+ setattr(self, modified_key, value)
208
+ updated = True
209
+ else:
210
+ # If the key doesn't match any attribute, update self.entry directly
211
+ self.entry[key] = value
212
+ updated = True
213
+
214
+ return updated
215
+
216
+ def compare(self, obj2: 'ObjectTab') -> bool:
217
+ """
218
+ Compare two objects to see if their values are the same based on CompareAttributeList.
219
+ """
220
+ if not isinstance(obj2, type(self)):
221
+ raise ValueError(f'Expected object of type {type(self).__name__}, got {type(obj2).__name__} instead.')
222
+
223
+ return all(
224
+ getattr(self, attr, "").lower() == getattr(obj2, attr, "").lower() if isinstance(getattr(self, attr, None),
225
+ str)
226
+ else getattr(self, attr) == getattr(obj2, attr)
227
+ for attr in self.CompareAttributeList
228
+ )
229
+
230
+
231
+ class Addresses(Object):
232
+ """
233
+ Manages address objects that allow you to reuse the same object as a source or destination address across all the
234
+ policy rulebases without having to add the address manually each time. An address object is an entity in which you
235
+ can include IPv4 addresses, IPv6 addresses (a single IP address, a range of addresses, or a subnet) or FQDNs.
236
+ """
237
+ address_types = ['ip-netmask', 'ip-range', 'ip-wildcard', 'fqdn']
238
+ # This is a list of attributes that need to be compared to determine if 2 objects are the same.
239
+ CompareAttributeList = ['value'].extend(address_types)
240
+
241
+ def __init__(self, PANDevice: Panorama | Firewall, **kwargs):
242
+ self._ip_netmask = None
243
+ self._ip_range = None
244
+ self._ip_wildcard = None
245
+ self._fqdn = None
246
+ # Preprocess kwargs to identify and handle address type
247
+ addr_type_key = next((key for key in self.address_types if key in kwargs), 'ip-netmask')
248
+ addr_type_value = kwargs.pop(addr_type_key, None)
249
+ super().__init__(PANDevice, max_name_length=64, max_description_length=1024, has_tags=True, **kwargs)
250
+ self.value = addr_type_value if addr_type_value else ''
251
+
252
+ def validate_addr_type(self, val: str):
253
+ if val not in self.address_types:
254
+ raise ValueError(f'Invalid type. Must be one of {self.address_types}')
255
+
256
+ def validate_value(self, val: str):
257
+ if val:
258
+ try:
259
+ if self.AddrType in ['ip-netmask', 'ip-range']:
260
+ ipaddress.ip_network(val, strict=False)
261
+ elif self.AddrType == 'ip-wildcard':
262
+ # Assuming validation logic for wildcard
263
+ pass
264
+ elif self.AddrType == 'fqdn':
265
+ if not re.match(r"^[0-9a-zA-Z.-]{,255}$", val):
266
+ raise ValueError(
267
+ 'Invalid character in name. Only [0-9a-zA-Z.-] are allowed and must be less than 255 characters.')
268
+ except ValueError as e:
269
+ raise ValueError(f'Validation error for {self.AddrType} with value {val}: {e}')
270
+
271
+ @property
272
+ def ip_netmask(self):
273
+ return self._ip_netmask
274
+
275
+ @ip_netmask.setter
276
+ def ip_netmask(self, value: str):
277
+ self.validate_ip_netmask(value)
278
+ self._set_address_type('ip_netmask', value)
279
+
280
+ @property
281
+ def ip_range(self):
282
+ return self._ip_range
283
+
284
+ @ip_range.setter
285
+ def ip_range(self, value: str):
286
+ self.validate_ip_range(value)
287
+ self._set_address_type('ip_range', value)
288
+
289
+ @property
290
+ def ip_wildcard(self):
291
+ return self._ip_wildcard
292
+
293
+ @ip_wildcard.setter
294
+ def ip_wildcard(self, value: str):
295
+ # Add validation for ip_wildcard as needed
296
+ self._set_address_type('ip_wildcard', value)
297
+
298
+ @property
299
+ def fqdn(self):
300
+ return self._fqdn
301
+
302
+ @fqdn.setter
303
+ def fqdn(self, value: str):
304
+ self.validate_fqdn(value)
305
+ self._set_address_type('fqdn', value)
306
+
307
+ def _set_address_type(self, addr_type: str, value: str):
308
+ """
309
+ Sets the address based on the addr_type and clears other address type attributes.
310
+
311
+ :param addr_type: The type of address to set ('ip-netmask', 'ip-range', 'ip-wildcard', 'fqdn').
312
+ :param value: The address value to set.
313
+ """
314
+ if addr_type == 'ip_netmask':
315
+ self.validate_ip_netmask(value)
316
+ self._clear_address_attrs()
317
+ setattr(self, f"_{addr_type}", value)
318
+ self.entry.update({'ip-netmask': value})
319
+ elif addr_type == 'ip_range':
320
+ self.validate_ip_range(value)
321
+ self._clear_address_attrs()
322
+ setattr(self, f"_{addr_type}", value)
323
+ self.entry.update({'ip-range': value})
324
+ elif addr_type == 'ip_wildcard':
325
+ # Add validation if necessary
326
+ self._clear_address_attrs()
327
+ setattr(self, f"_{addr_type}", value)
328
+ self.entry.update({'ip-wildcard': value})
329
+ elif addr_type == 'fqdn':
330
+ self.validate_fqdn(value)
331
+ self._clear_address_attrs()
332
+ setattr(self, f"_{addr_type}", value)
333
+ self.entry.update({'fqdn': value})
334
+ else:
335
+ raise ValueError(f"Invalid address type: {addr_type}")
336
+
337
+ def _clear_address_attrs(self):
338
+ """Clears all address type attributes."""
339
+ for attr in ['_ip_netmask', '_ip_range', '_ip_wildcard', '_fqdn']:
340
+ setattr(self, attr, None)
341
+ for address_type in self.address_types:
342
+ # Remove the address type from the entry if it exists
343
+ self.entry.pop(address_type, None)
344
+
345
+ def validate_ip_netmask(self, value: str):
346
+ try:
347
+ ipaddress.ip_network(value, strict=False)
348
+ except ValueError:
349
+ raise ValueError(f"Invalid ip-netmask value: {value}")
350
+
351
+ def validate_ip_range(self, value: str):
352
+ if '-' not in value:
353
+ raise ValueError("IP range must contain '-' as a separator")
354
+ start, end = value.split('-', 1)
355
+ try:
356
+ ipaddress.ip_address(start)
357
+ ipaddress.ip_address(end)
358
+ except ValueError:
359
+ raise ValueError(f"Invalid IP range: {value}")
360
+
361
+ def validate_fqdn(self, value: str):
362
+ if not re.match(r'^[a-zA-Z\d-]{,63}(\.[a-zA-Z\d-]{,63})*$', value):
363
+ raise ValueError(f"Invalid FQDN: {value}")
364
+
365
+ @property
366
+ def value(self):
367
+ return self._value
368
+
369
+ @value.setter
370
+ def value(self, val):
371
+ self.validate_value(val)
372
+ self._value = val
373
+
374
+
375
+ class AddressGroups(Object):
376
+ """
377
+ MemberObj: list of AddressObjects assigned to this list.
378
+ """
379
+
380
+ valid_types = ['static', 'dynamic']
381
+ CompareAttributeList = ['member', 'filter'].extend(valid_types)
382
+
383
+ def __init__(self, PANDevice, **kwargs):
384
+ super().__init__(PANDevice, max_name_length=64, max_description_length=1024, has_tags=False, **kwargs)
385
+ # Initialize MemberObj list with Address objects if 'member' list is provided
386
+ self.MemberObj: list = []
387
+ self.static: Dict[str, List[str]] = {}
388
+ self.dynamic: Dict[str, List[str]] = {}
389
+
390
+ @property
391
+ def static(self):
392
+ return self._static
393
+
394
+ @static.setter
395
+ def static(self, value: dict):
396
+ """
397
+ The setter method for the 'static' attribute.
398
+ Validates that the value is a dictionary containing the key 'member' with a value of type list,
399
+ and this list contains at least one item.
400
+
401
+ :param value: The value to set for the 'static' attribute.
402
+ :type value: dict
403
+ :raises ValueError: If the value does not meet the specified criteria.
404
+ """
405
+ if not isinstance(value, dict):
406
+ raise ValueError("The 'static' attribute must be a dictionary.")
407
+
408
+ if 'member' not in value:
409
+ raise ValueError("The dictionary must contain the key 'member'.")
410
+
411
+ if not isinstance(value['member'], list):
412
+ raise ValueError("The 'member' key must have a list as its value.")
413
+
414
+ if len(value['member']) < 1:
415
+ raise ValueError("The list under 'member' key must contain at least one item.")
416
+
417
+ self._static = value
418
+ self.entry.update({'static': value})
419
+
420
+ @property
421
+ def dynamic(self) -> dict:
422
+ """
423
+ The 'dynamic' property getter.
424
+ """
425
+ return self._dynamic
426
+
427
+ @dynamic.setter
428
+ def dynamic(self, value: dict) -> None:
429
+ """
430
+ The setter for the 'dynamic' attribute.
431
+ Validates that the value is a dictionary with a key 'filter' whose value is a string of max length 2047.
432
+
433
+ :param value: The dictionary to set as the 'dynamic' attribute.
434
+ :raises ValueError: If the value does not meet the specified criteria.
435
+ """
436
+ # Check if the value is a dictionary
437
+ if not isinstance(value, dict):
438
+ raise ValueError("The 'dynamic' attribute must be a dictionary.")
439
+
440
+ # Check if the dictionary has a key named 'filter'
441
+ if 'filter' not in value:
442
+ raise ValueError("The dictionary must contain the key 'filter'.")
443
+
444
+ # Check if the value of 'filter' is a string
445
+ if not isinstance(value['filter'], str):
446
+ raise ValueError("The 'filter' key must have a string as its value.")
447
+
448
+ # Check if the string length of 'filter' is within the limit
449
+ if len(value['filter']) > 2047:
450
+ raise ValueError("The 'filter' value must not exceed 2047 characters.")
451
+
452
+ # If all checks pass, set the value
453
+ self._dynamic = value
454
+ self.entry.update({'dynamic': value})
455
+
456
+ @property
457
+ def MemberObj(self):
458
+ return self._MemberObj
459
+
460
+ @MemberObj.setter
461
+ def MemberObj(self, val):
462
+ if val is not None:
463
+ if not isinstance(val, list):
464
+ raise TypeError(f'Attribute {sys._getframe().f_code.co_name} must be of type list.')
465
+ self._MemberObj = val
466
+
467
+ @MemberObj.deleter
468
+ def MemberObj(self):
469
+ del self._MemberObj
470
+
471
+ def add_member(self, member):
472
+ if not hasattr(self, 'static'):
473
+ raise ValueError("Can only add members to a 'static' type AddressGroup")
474
+ if isinstance(member, Addresses):
475
+ if member.name not in self.member:
476
+ self.member.append(member.name)
477
+ self.MemberObj.append(member)
478
+ elif isinstance(member, str):
479
+ if member not in self.member:
480
+ self.member.append(member)
481
+ # Optionally, resolve the Addresses object from the name and add to MemberObj
482
+ else:
483
+ raise TypeError("Member must be an Addresses object or a string")
484
+
485
+ def remove_member(self, member):
486
+ if not hasattr(self, 'static'):
487
+ raise ValueError("Can only remove members from a 'static' type AddressGroup")
488
+ if isinstance(member, Addresses):
489
+ member_name = member.name
490
+ elif isinstance(member, str):
491
+ member_name = member
492
+ else:
493
+ raise TypeError("Member must be an Addresses object or a string")
494
+
495
+ if member_name in self.member:
496
+ self.member.remove(member_name)
497
+ self.MemberObj = [obj for obj in self.MemberObj if obj.name != member_name]
498
+
499
+ def set_filter(self, filter_str):
500
+ if not hasattr(self, 'dynamic'):
501
+ raise ValueError("Filter can only be set for a 'dynamic' type AddressGroup")
502
+ if len(filter_str) > 2047:
503
+ raise ValueError("Filter length exceeds the maximum allowed characters (2047)")
504
+ self.dynamic = filter_str
505
+
506
+ def get_object(self, obj_type, name: str, location: str, device_group: str, vsys: str) -> Optional[Any]:
507
+ """
508
+ Attempt to instantiate an object of type `obj_type` with the provided parameters.
509
+ Returns the instantiated object if it exists, None otherwise.
510
+
511
+ :param obj_type: The class of the object to be instantiated.
512
+ :param name: The name of the object.
513
+ :param location: The location of the object.
514
+ :param device_group: The device group the object belongs to.
515
+ :param vsys: The virtual system the object belongs to.
516
+ :return: An instance of `obj_type` if successful, None otherwise.
517
+ """
518
+ try:
519
+ obj = obj_type(PANDevice=self.PANDevice, name=name, location=location, device_group=device_group, vsys=vsys)
520
+ if obj.get(ANYLOCATION=True, IsSearch=True):
521
+ return obj
522
+ except Exception as e:
523
+ # Here you should log the exception e
524
+ logger.error(f"Error instantiating object of type {obj_type.__name__}: {e}")
525
+ return None
526
+
527
+ def populate(self) -> None:
528
+ """
529
+ Populate the MemberObjs list with Addresses or AddressGroups from the firewall.
530
+ Tries to instantiate Address objects first; if that fails, tries AddressGroups.
531
+ Raises an exception if the static member list is empty or the objects cannot be found.
532
+ """
533
+ members = self.static.get('member')
534
+ if not members:
535
+ raise ValueError('Cannot populate an empty group.')
536
+
537
+ for item in members:
538
+ new_object = self.get_object(Addresses, item, location=self.location, device_group=self.device_group,
539
+ vsys=self.vsys)
540
+ if new_object and new_object.refresh():
541
+ self.MemberObj.append(new_object)
542
+ else:
543
+ new_object = self.get_object(AddressGroups, item, location=self.location,
544
+ device_group=self.device_group, vsys=self.vsys)
545
+ if new_object and new_object.refresh():
546
+ self.MemberObj.append(new_object)
547
+ else:
548
+ logger.error(f'Could not find {item} in {self.PANDevice.IP}')
549
+
550
+
551
+ class Regions(Object):
552
+ CompareAttributeList = ['latitude', 'longitude', 'address']
553
+
554
+ def __init__(self, PANDevice, **kwargs):
555
+ super().__init__(PANDevice, max_name_length=32, has_tags=False, **kwargs)
556
+ self.geo_location = kwargs.get('geo_location')
557
+ self.address = kwargs.get('address')
558
+
559
+ @property
560
+ def geo_location(self) -> Union[None, dict]:
561
+ return self.entry.get('geo-location')
562
+
563
+ @geo_location.setter
564
+ def geo_location(self, value: Union[None, dict]):
565
+ if value is not None:
566
+ if not isinstance(value, dict):
567
+ raise TypeError('geo_location must be a dictionary.')
568
+
569
+ if 'latitude' in value and 'longitude' in value:
570
+ latitude = value['latitude']
571
+ longitude = value['longitude']
572
+
573
+ if not isinstance(latitude, float) or not (-90 <= latitude <= 90):
574
+ raise ValueError('Latitude must be a float between -90 and 90.')
575
+
576
+ if not isinstance(longitude, float) or not (-180 <= longitude <= 180):
577
+ raise ValueError('Longitude must be a float between -180 and 180.')
578
+ self._geo_location = value
579
+ self.entry['geo-location'] = {'latitude': latitude, 'longitude': longitude}
580
+ else:
581
+ raise ValueError('geo_location dictionary must contain both latitude and longitude keys.')
582
+
583
+ @property
584
+ def address(self) -> List[str]:
585
+ return self._address
586
+
587
+ @address.setter
588
+ def address(self, value: List[str]):
589
+ if value:
590
+ if not isinstance(value, list):
591
+ raise TypeError('The address attribute must be of type list.')
592
+ for addr in value:
593
+ if '/' in addr:
594
+ try:
595
+ ipaddress.IPv4Network(addr, strict=False)
596
+ except (ipaddress.AddressValueError, ipaddress.NetmaskValueError):
597
+ raise ValueError(f'The IP address {addr} is not valid.')
598
+ elif '-' in addr:
599
+ left_val, right_val = addr.split('-')
600
+ try:
601
+ ipaddress.IPv4Address(left_val)
602
+ ipaddress.IPv4Address(right_val)
603
+ except ipaddress.AddressValueError:
604
+ raise ValueError(f'The IP address range {addr} is not valid.')
605
+ else:
606
+ try:
607
+ ipaddress.IPv4Address(addr)
608
+ except ipaddress.AddressValueError:
609
+ raise ValueError(f'The IP address {addr} is not valid.')
610
+ self._address = value
611
+ self.entry.update({'address': {'member': value}})
612
+
613
+
614
+ class DynamicUserGroups(Object):
615
+ """
616
+ Manages dynamic user groups. With dynamic user groups, you can use tags to define groups that automatically contain
617
+ users who match the criteria you define. These groups enable you to mitigate risk when the data about a security
618
+ incident contains only a username. They also allow you to link users with information from log forwarding and risk
619
+ assessment applications, providing a more complete view of user behavior than you would see with user directories
620
+ alone.
621
+ """
622
+ CompareAttributeList = ['filter']
623
+
624
+ def __init__(self, PANDevice, **kwargs):
625
+ super().__init__(PANDevice, max_name_length=64, max_description_length=1024, has_tags=True,
626
+ **kwargs)
627
+ self.filter = kwargs.get('Filter')
628
+
629
+ @property
630
+ def filter(self) -> str:
631
+ return self._filter
632
+
633
+ @filter.setter
634
+ def filter(self, value: str) -> None:
635
+ if value is not None:
636
+ if not isinstance(value, str):
637
+ raise TypeError('The filter attribute must be of type str.')
638
+ if len(value) > 2047:
639
+ raise ValueError('Filter string is too long. Maximum number of characters is 2047.')
640
+ self._filter = value
641
+ if value is not None:
642
+ self.entry.update({'filter': value})
643
+ else:
644
+ self.entry.pop('filter', None)
645
+
646
+
647
+ class Applications(Object):
648
+ valid_types = ['port', 'ident-by-ip-protocol', 'ident-by-icmp-type', 'ident-by-icmp6-type']
649
+ valid_risk = [1, 2, 3, 4, 5]
650
+
651
+ # This is a list of attributes that need to be compared to determine if 2 objects are the same.
652
+ CompareAttributeList = ['default', 'subcategory', 'technology', 'risk']
653
+
654
+ def __init__(self, PANDevice, **kwargs):
655
+ super().__init__(PANDevice, max_name_length=32, max_description_length=1024, has_tags=False, **kwargs)
656
+ self.default: dict = kwargs.get('default')
657
+ self.category: str = kwargs.get('category')
658
+ self.subcategory: str = kwargs.get('subcategory')
659
+ self.technology: str = kwargs.get('technology')
660
+ self.timeout: int = kwargs.get('timeout')
661
+ self.tcp_timeout: int = kwargs.get('tcp_timeout')
662
+ self.udp_timeout: int = kwargs.get('udp_timeout')
663
+ self.tcp_half_closed_timeout: int = kwargs.get('tcp_half_closed_timeout')
664
+ self.tcp_time_wait_timeout: int = kwargs.get('tcp_time_wait_timeout')
665
+ self.risk: int = kwargs.get('risk')
666
+ self.evasive_behaviour: str = kwargs.get('evasive_behaviour')
667
+ self.consume_big_bandwidth: str = kwargs.get('consume_big_bandwidth')
668
+ self.used_by_malware: str = kwargs.get('used_by_malware')
669
+ self.able_to_transfer_file: str = kwargs.get('able_to_transfer_file')
670
+ self.has_known_vulnerability: str = kwargs.get('has_known_vulnerability')
671
+ self.tunnel_other_application: str = kwargs.get('tunnel_other_application')
672
+ self.tunnel_applications: str = kwargs.get('tunnel_applications')
673
+ self.prone_to_misuse: str = kwargs.get('prone_to_misuse')
674
+ self.pervasive_use: str = kwargs.get('pervasive_use')
675
+ self.file_type_ident: str = kwargs.get('file_type_ident')
676
+ self.virus_ident: str = kwargs.get('virus_ident')
677
+ self.data_ident: str = kwargs.get('data_ident')
678
+ self.no_appid_caching: str = kwargs.get('no_appid_caching')
679
+ self.alg_disable_capability: str = kwargs.get('alg_disable_capability', '')
680
+ self.parent_app: str = kwargs.get('parent_app', '')
681
+ self.signature = [SignatureEntry(**signature) for signature in kwargs.get('signatures', [])]
682
+
683
+ # If signatures are provided, add them to the entry dictionary
684
+ if 'signature' in kwargs:
685
+ self.entry.update({'signature': {'entry': [signature.to_dict() for signature in self.signatures]}})
686
+
687
+ def set_yes_no_attribute(self, attribute_name: str, value: str):
688
+ if not isinstance(value, str) or value.lower() not in ['yes', 'no']:
689
+ raise ValueError(f"{attribute_name} must be 'yes' or 'no'.")
690
+ setattr(self, f'_{attribute_name}', value.lower())
691
+ self.entry.update({attribute_name.replace('_', '-'): value.lower()})
692
+
693
+ @property
694
+ def default(self) -> dict:
695
+ return self._default
696
+
697
+ @default.setter
698
+ def default(self, value: dict):
699
+ if not isinstance(value, dict):
700
+ raise TypeError("Default must be a dictionary.")
701
+
702
+ for key, val in value.items():
703
+ if key not in self.valid_types:
704
+ raise ValueError(f"Invalid type: {key}. Must be one of: {', '.join(self.valid_types)}")
705
+
706
+ if key == 'port':
707
+ if not isinstance(val, dict) or 'member' not in val or not isinstance(val['member'], list):
708
+ raise ValueError(
709
+ "For 'port', the value must be a dictionary with a 'member' key containing a list of strings.")
710
+ if not all(isinstance(item, str) for item in val['member']):
711
+ raise ValueError("All items in the 'member' list for 'port' must be strings.")
712
+
713
+ elif key == 'ident-by-ip-protocol':
714
+ if not isinstance(val, str):
715
+ raise ValueError(f"The value for {key} must be a string.")
716
+
717
+ elif key in ['ident-by-icmp-type', 'ident-by-icmp6-type']:
718
+ if not isinstance(val, dict) or 'type' not in val or 'code' not in val:
719
+ raise ValueError(f"For {key}, the value must be a dictionary with 'type' and 'code' keys.")
720
+ if not isinstance(val['type'], str) or not isinstance(val['code'], str):
721
+ raise ValueError(f"Both 'type' and 'code' values for {key} must be strings.")
722
+
723
+ self._default = value
724
+ self.entry.update({'default': value})
725
+
726
+ @property
727
+ def risk(self) -> int:
728
+ return self._risk
729
+
730
+ @risk.setter
731
+ def risk(self, value: int):
732
+ if not isinstance(value, int) or not (1 <= value <= 5):
733
+ raise ValueError("Risk must be an integer between 1 and 5.")
734
+ self._risk = value
735
+ self.entry.update({'risk': value})
736
+
737
+ @property
738
+ def category(self) -> str:
739
+ return self._category
740
+
741
+ @category.setter
742
+ def category(self, value: str):
743
+ if not isinstance(value, str):
744
+ raise TypeError("Category must be a string.")
745
+ self._category = value
746
+ self.entry.update({'category': value})
747
+
748
+ @property
749
+ def subcategory(self) -> str:
750
+ return self._subcategory
751
+
752
+ @subcategory.setter
753
+ def subcategory(self, value: str):
754
+ if not isinstance(value, str) or len(value) > 63:
755
+ raise ValueError("Subcategory must be a string up to 63 characters long.")
756
+ self._subcategory = value
757
+ self.entry.update({'subcategory': value})
758
+
759
+ @property
760
+ def technology(self) -> str:
761
+ return self._technology
762
+
763
+ @technology.setter
764
+ def technology(self, value: str):
765
+ if not isinstance(value, str) or len(value) > 63:
766
+ raise ValueError("Technology must be a string up to 63 characters long.")
767
+ self._technology = value
768
+ self.entry.update({'technology': value})
769
+
770
+ @property
771
+ def evasive_behavior(self):
772
+ return self._evasive_behavior
773
+
774
+ @evasive_behavior.setter
775
+ def evasive_behavior(self, value: str):
776
+ self.set_yes_no_attribute('evasive_behavior', value)
777
+
778
+ @property
779
+ def consume_big_bandwidth(self):
780
+ return self._consume_big_bandwidth
781
+
782
+ @consume_big_bandwidth.setter
783
+ def consume_big_bandwidth(self, value: str):
784
+ self.set_yes_no_attribute('consume_big_bandwidth', value)
785
+
786
+ @property
787
+ def used_by_malware(self):
788
+ return self._used_by_malware
789
+
790
+ @used_by_malware.setter
791
+ def used_by_malware(self, value: str):
792
+ self.set_yes_no_attribute('used_by_malware', value)
793
+
794
+ @property
795
+ def able_to_transfer_file(self):
796
+ return self._able_to_transfer_file
797
+
798
+ @able_to_transfer_file.setter
799
+ def able_to_transfer_file(self, value: str):
800
+ self.set_yes_no_attribute('able_to_transfer_file', value)
801
+
802
+ @property
803
+ def has_known_vulnerability(self):
804
+ return self._has_known_vulnerability
805
+
806
+ @has_known_vulnerability.setter
807
+ def has_known_vulnerability(self, value: str):
808
+ self.set_yes_no_attribute('has_known_vulnerability', value)
809
+
810
+ @property
811
+ def tunnel_other_application(self):
812
+ return self._tunnel_other_application
813
+
814
+ @tunnel_other_application.setter
815
+ def tunnel_other_application(self, value: str):
816
+ self.set_yes_no_attribute('tunnel_other_application', value)
817
+
818
+ @property
819
+ def tunnel_applications(self):
820
+ return self._tunnel_applications
821
+
822
+ @tunnel_applications.setter
823
+ def tunnel_applications(self, value: str):
824
+ self.set_yes_no_attribute('tunnel_applications', value)
825
+
826
+ @property
827
+ def prone_to_misuse(self):
828
+ return self._prone_to_misuse
829
+
830
+ @prone_to_misuse.setter
831
+ def prone_to_misuse(self, value: str):
832
+ self.set_yes_no_attribute('prone_to_misuse', value)
833
+
834
+ @property
835
+ def pervasive_use(self):
836
+ return self._pervasive_use
837
+
838
+ @pervasive_use.setter
839
+ def pervasive_use(self, value: str):
840
+ self.set_yes_no_attribute('pervasive_use', value)
841
+
842
+ @property
843
+ def file_type_ident(self):
844
+ return self._file_type_ident
845
+
846
+ @file_type_ident.setter
847
+ def file_type_ident(self, value: str):
848
+ self.set_yes_no_attribute('file_type_ident', value)
849
+
850
+ @property
851
+ def virus_ident(self):
852
+ return self._virus_ident
853
+
854
+ @virus_ident.setter
855
+ def virus_ident(self, value: str):
856
+ self.set_yes_no_attribute('virus_ident', value)
857
+
858
+ @property
859
+ def data_ident(self):
860
+ return self._data_ident
861
+
862
+ @data_ident.setter
863
+ def data_ident(self, value: str):
864
+ self.set_yes_no_attribute('data_ident', value)
865
+
866
+ @property
867
+ def no_appid_caching(self):
868
+ return self._no_appid_caching
869
+
870
+ @no_appid_caching.setter
871
+ def no_appid_caching(self, value: str):
872
+ self.set_yes_no_attribute('no_appid_caching', value)
873
+
874
+ # Assuming alg_disable_capability is a string and not a yes/no field
875
+ @property
876
+ def alg_disable_capability(self):
877
+ return self._alg_disable_capability
878
+
879
+ @alg_disable_capability.setter
880
+ def alg_disable_capability(self, value: str):
881
+ if not isinstance(value, str) or len(value) > 127:
882
+ raise ValueError("alg_disable_capability must be a string up to 127 characters long.")
883
+ self._alg_disable_capability = value
884
+ self.entry.update({'alg-disable-capability': value})
885
+
886
+ # Assuming parent_app is a string and not a yes/no field
887
+ @property
888
+ def parent_app(self):
889
+ return self._parent_app
890
+
891
+ @parent_app.setter
892
+ def parent_app(self, value: str):
893
+ if not isinstance(value, str) or len(value) > 127:
894
+ raise ValueError("parent_app must be a string up to 127 characters long.")
895
+ self._parent_app = value
896
+ self.entry.update({'parent-app': value})
897
+
898
+
899
+ class ApplicationGroups(Object):
900
+ CompareAttributeList = ['members']
901
+
902
+ def __init__(self, PANDevice, **kwargs):
903
+ super().__init__(PANDevice, max_name_length=31, has_tags=False, **kwargs)
904
+ self.members: Dict[str, List[str]] = kwargs.get('members', {'member': []})
905
+ self.MemberObj = []
906
+
907
+ @property
908
+ def members(self) -> Dict[str, List[str]]:
909
+ return self._members
910
+
911
+ @members.setter
912
+ def members(self, value: Dict[str, List[str]]) -> None:
913
+ self.validate_member_dict(value, 'members')
914
+ self._members = value
915
+ self.entry.update({'members': value})
916
+
917
+
918
+ class ApplicationFilters(Object):
919
+ def __init__(self, PANDevice, **kwargs):
920
+ super().__init__(PANDevice, MaxNameLength=31, HasTags=True, **kwargs)
921
+
922
+
923
+ class Services(Object):
924
+ """
925
+ Manages service objects, which are used to identify network services that applications can or can not use.
926
+ Network services can be defined based on protocols and/or ports. You can also use service objects to define session
927
+ timeout values.
928
+ """
929
+ valid_types = ['tcp', 'udp']
930
+
931
+ # This is a list of attributes that need to be compared to determine if 2 objects are the same.
932
+ CompareAttributeList = ['protocol']
933
+
934
+ def __init__(self, PANDevice, **kwargs):
935
+ super().__init__(PANDevice, max_name_length=64, max_description_length=1024, has_tags=True, **kwargs)
936
+ self.protocol: Dict = kwargs.get('protocol')
937
+
938
+ @property
939
+ def protocol(self) -> dict:
940
+ return self._protocol
941
+
942
+ @protocol.setter
943
+ def protocol(self, value: dict) -> None:
944
+ if value:
945
+ if not isinstance(value, dict):
946
+ raise TypeError("Protocol must be a dictionary.")
947
+
948
+ validated_protocols = {}
949
+ for protocol, settings in value.items():
950
+ if protocol not in self.valid_types:
951
+ raise ValueError(f"Invalid protocol: {protocol}. Must be one of: {', '.join(self.valid_types)}")
952
+
953
+ if 'port' not in settings:
954
+ raise ValueError(f"'port' key is required for {protocol}.")
955
+
956
+ protocol_settings = {'port': settings['port']} # 'port' is mandatory
957
+
958
+ # Validate and include optional 'source-port'
959
+ if 'source-port' in settings and isinstance(settings['source-port'], str):
960
+ protocol_settings['source-port'] = settings['source-port']
961
+
962
+ # Initialize 'override' if it's valid
963
+ if 'override' in settings and isinstance(settings['override'], dict):
964
+ override_settings: dict = {}
965
+ for override_key, override_value in settings['override'].items():
966
+ if override_key not in ['no', 'yes']:
967
+ raise ValueError(f"Invalid override key: {override_key}. Must be 'no' or 'yes'.")
968
+
969
+ if override_key == 'yes' and isinstance(override_value, dict):
970
+ # Validate and include timeout settings
971
+ for timeout_key in ['timeout', 'halfclose_timeout', 'timewait_timeout']:
972
+ if timeout_key in override_value and isinstance(override_value[timeout_key], int):
973
+ if (timeout_key == 'timewait_timeout' and 1 <= override_value[
974
+ timeout_key] <= 600) or \
975
+ (timeout_key != 'timewait_timeout' and 1 <= override_value[
976
+ timeout_key] <= 604800):
977
+ # Replace underscore with dash in the timeout key
978
+ formatted_timeout_key = timeout_key.replace('_', '-')
979
+ override_settings[formatted_timeout_key] = override_value[timeout_key]
980
+
981
+ if override_settings:
982
+ protocol_settings['override'] = {'yes': override_settings}
983
+ elif override_key == 'no':
984
+ protocol_settings['override'] = {'no': {}}
985
+
986
+ validated_protocols[protocol] = protocol_settings
987
+
988
+ # Replace underscores with dashes for keys in self.entry
989
+ formatted_protocols = {k.replace('_', '-'): v for k, v in validated_protocols.items()}
990
+
991
+ self._protocol = validated_protocols
992
+ self.entry.update({'protocol': formatted_protocols})
993
+
994
+ class ServiceGroups(Object):
995
+ """
996
+ Manages service groups. To simplify creation of security policies, service objects can be grouped.
997
+ Add service objects to a group using the object's name.
998
+ """
999
+ CompareAttributeList = ['members']
1000
+
1001
+ def __init__(self, PANDevice, **kwargs):
1002
+ super().__init__(PANDevice, MaxNameLength=64, HasTags=True, **kwargs)
1003
+ self.members: Dict[str, List[str]] = kwargs.get('members', {'member': []})
1004
+
1005
+ @property
1006
+ def members(self) -> Dict[str, List[str]]:
1007
+ return self._members
1008
+
1009
+ @members.setter
1010
+ def members(self, value: Dict[str, List[str]]) -> None:
1011
+ self.validate_member_dict(value, 'members')
1012
+ self._members = value
1013
+ self.entry.update({'members': value})
1014
+
1015
+ class Tags(Object):
1016
+ valid_colors: List[str] = ['color1', 'color2', 'color3', 'color4', 'color5', 'color6', 'color7', 'color8', 'color9',
1017
+ 'color10',
1018
+ 'color11', 'color12', 'color13', 'color14', 'color15', 'color16', 'color17', 'color18',
1019
+ 'color19',
1020
+ 'color20', 'color21', 'color22', 'color23', 'color24', 'color25', 'color26', 'color27',
1021
+ 'color28',
1022
+ 'color29', 'color30', 'color31', 'color32', 'color33', 'color34', 'color35', 'color36',
1023
+ 'color37',
1024
+ 'color38', 'color39', 'color40', 'color41', 'color42']
1025
+
1026
+ CompareAttributeList = ['name']
1027
+
1028
+ def __init__(self, PANDevice, **kwargs):
1029
+ super().__init__(PANDevice, max_name_length=128, has_tags=False, **kwargs)
1030
+ self.color: str = kwargs.get('color', None)
1031
+ self.comments: str = kwargs.get('comments', '')
1032
+
1033
+ @property
1034
+ def comments(self):
1035
+ return self._comments
1036
+
1037
+ @comments.setter
1038
+ def comments(self, value: str) -> None:
1039
+ if not isinstance(value, str):
1040
+ raise TypeError(f'Attribute comments must be of type str, not {type(value).__name__}.')
1041
+ if len(value) > 1023:
1042
+ raise ValueError(
1043
+ f'Comment cannot exceed 1023 characters; provided comment is {len(value)} characters long.')
1044
+ self._comments = value
1045
+ self.entry.update({'comments': value})
1046
+
1047
+ @property
1048
+ def color(self):
1049
+ return self._color
1050
+
1051
+ @color.setter
1052
+ def color(self, value: str) -> None:
1053
+ if value is not None:
1054
+ if not isinstance(value, str):
1055
+ raise TypeError(f'Attribute color must be of type str, not {type(value).__name__}.')
1056
+ if value not in self.valid_colors:
1057
+ raise ValueError(f'Invalid color: {value}. Must be one of: {", ".join(self.valid_colors)}.')
1058
+ self._color = value
1059
+ self.entry.update({'color': value})
1060
+ else:
1061
+ self._color = None
1062
+
1063
+ class ExternalDynamicLists(Object):
1064
+ CompareAttributeList = ['type']
1065
+ predefined_lists = ['panw-torexit-ip-list', 'panw-bulletproof-ip-list',
1066
+ 'panw-highrisk-ip-list', 'panw-known-ip-list',
1067
+ 'panw-auth-portal-exclude-list']
1068
+ valid_types = ['predefined-ip', 'predefined-url', 'ip', 'domain', 'url', 'imsi', 'imei']
1069
+
1070
+ def __init__(self, PANDevice, **kwargs):
1071
+ super().__init__(PANDevice, max_name_length=64, max_description_length=256, has_tags=False,
1072
+ **kwargs)
1073
+ self.type_: Dict = kwargs.get('type', {})
1074
+
1075
+ @property
1076
+ def type_(self) -> Optional[Dict]:
1077
+ return self._type_
1078
+
1079
+ @type_.setter
1080
+ def type_(self, value: Dict) -> None:
1081
+ if not isinstance(value, dict):
1082
+ raise TypeError("Type must be a dictionary.")
1083
+
1084
+ for key, val in value.items():
1085
+ if key not in self.valid_types:
1086
+ raise ValueError(f"Invalid type: {key}. Must be one of: {', '.join(self.valid_types)}")
1087
+
1088
+ if key in ['predefined-ip', 'predefined-url']:
1089
+ self._validate_predefined_type(val, key)
1090
+ elif key in ['ip', 'domain', 'url', 'imsi', 'imei']:
1091
+ self._validate_dynamic_type(val, key)
1092
+
1093
+ self._type_ = value
1094
+ self.entry.update({'type': self._type_})
1095
+
1096
+ def _validate_predefined_type(self, val: Dict, type_key: str) -> None:
1097
+ required_keys = ['exception-list', 'description', 'url']
1098
+ for key in required_keys:
1099
+ if key not in val:
1100
+ raise ValueError(f"Missing '{key}' in '{type_key}' type")
1101
+
1102
+ # Add specific validation for each key as needed...
1103
+
1104
+ def _validate_dynamic_type(self, val: Dict, type_key: str) -> None:
1105
+ # For 'ip', 'domain', 'url', 'imsi', 'imei', only 'url' and 'recurring' are required
1106
+ if 'url' not in val:
1107
+ raise ValueError(f"'url' is required for '{type_key}' type")
1108
+
1109
+ if 'recurring' not in val:
1110
+ raise ValueError(f"'recurring' is required for '{type_key}' type")
1111
+
1112
+ # Validate URL
1113
+ if not isinstance(val['url'], str) or not re.match(r'^https?://', val['url']):
1114
+ raise ValueError("Invalid 'url' format.")
1115
+
1116
+ # Validate 'exception-list' if present
1117
+ if 'exception-list' in val:
1118
+ if not isinstance(val['exception-list'], dict) or 'member' not in val['exception-list']:
1119
+ raise ValueError("Invalid 'exception-list' format.")
1120
+ if not all(isinstance(item, str) for item in val['exception-list']['member']):
1121
+ raise ValueError("All items in 'exception-list' must be strings.")
1122
+
1123
+ # Validate 'description' if present
1124
+ if 'description' in val:
1125
+ if not isinstance(val['description'], str) or len(val['description']) > 255:
1126
+ raise ValueError("Invalid 'description' format.")
1127
+
1128
+ # Validate 'certificate-profile' if present
1129
+ if 'certificate-profile' in val:
1130
+ if not isinstance(val['certificate-profile'], str):
1131
+ raise ValueError("Invalid 'certificate-profile' format.")
1132
+
1133
+ # Validate 'auth' if present
1134
+ if 'auth' in val:
1135
+ if not isinstance(val['auth'], dict):
1136
+ raise ValueError("Invalid 'auth' format.")
1137
+ if 'username' in val['auth'] and not 1 <= len(val['auth']['username']) <= 255:
1138
+ raise ValueError("Invalid 'username' format.")
1139
+ if 'password' in val['auth'] and not isinstance(val['auth']['password'], str):
1140
+ raise ValueError("Invalid 'password' format.")
1141
+ # Validate expand-domain if present
1142
+ if type_key == 'domain' and 'expand-domain' in val:
1143
+ if val['expand-domain'] not in ['yes', 'no']:
1144
+ raise ValueError("Invalid 'expand-domain' value. Must be 'yes' or 'no'.")
1145
+
1146
+ # Validate recurring
1147
+ self._validate_recurring(val['recurring'])
1148
+
1149
+ # Add validations for optional keys if they are present...
1150
+
1151
+ @staticmethod
1152
+ def _validate_recurring(recurring: Dict) -> None:
1153
+ valid_keys = ['five-minute', 'hourly', 'daily', 'weekly', 'monthly']
1154
+ if not set(recurring.keys()).issubset(set(valid_keys)):
1155
+ raise ValueError("Invalid keys in 'recurring'.")
1156
+
1157
+ # Check that 'hourly' and 'five-minute' have empty dictionaries as values
1158
+ for key in ['hourly', 'five-minute']:
1159
+ if key in recurring and recurring[key] != {}:
1160
+ raise ValueError(f"'{key}' should be an empty dictionary.")
1161
+
1162
+ # 'daily' validation
1163
+ if 'daily' in recurring:
1164
+ if not isinstance(recurring['daily'], dict) or 'at' not in recurring['daily']:
1165
+ raise ValueError("Invalid format for 'daily' in 'recurring'.")
1166
+ if not isinstance(recurring['daily']['at'], str) or not recurring['daily'][
1167
+ 'at'].isdigit() or not 0 <= int(recurring['daily']['at']) <= 23:
1168
+ raise ValueError("'at' in 'daily' should be a string representing an hour in the range 0-23.")
1169
+
1170
+ # 'weekly' validation
1171
+ if 'weekly' in recurring:
1172
+ if not isinstance(recurring['weekly'], dict) or 'day-of-week' not in recurring['weekly'] or 'at' not in \
1173
+ recurring['weekly']:
1174
+ raise ValueError("Invalid format for 'weekly' in 'recurring'.")
1175
+ if recurring['weekly']['day-of-week'].lower() not in ['sunday', 'monday', 'tuesday', 'wednesday',
1176
+ 'thursday', 'friday', 'saturday']:
1177
+ raise ValueError("Invalid 'day-of-week' in 'weekly'.")
1178
+ if not isinstance(recurring['weekly']['at'], str) or not recurring['weekly'][
1179
+ 'at'].isdigit() or not 0 <= int(recurring['weekly']['at']) <= 23:
1180
+ raise ValueError("'at' in 'weekly' should be a string representing an hour in the range 0-23.")
1181
+
1182
+ # 'monthly' validation
1183
+ if 'monthly' in recurring:
1184
+ if (not isinstance(recurring['monthly'], dict) or 'day-of-month' not in recurring['monthly'] or 'at' not in
1185
+ recurring['monthly']):
1186
+ raise ValueError("Invalid format for 'monthly' in 'recurring'.")
1187
+ if not isinstance(recurring['monthly']['day-of-month'], int) or not 1 <= recurring['monthly'][
1188
+ 'day-of-month'] <= 31:
1189
+ raise ValueError("'day-of-month' in 'monthly' should be an integer in the range 1-31.")
1190
+ if not isinstance(recurring['monthly']['at'], str) or not recurring['monthly'][
1191
+ 'at'].isdigit() or not 0 <= int(recurring['monthly']['at']) <= 23:
1192
+ raise ValueError("'at' in 'monthly' should be a string representing an hour in the range 0-23.")
1193
+
1194
+ class CustomURLCategories(Object):
1195
+ valid_types = ['URL List', 'Category Match']
1196
+ CompareAttributeList = ['type_', 'member']
1197
+
1198
+ def __init__(self, PANDevice, **kwargs):
1199
+ super().__init__(PANDevice, max_name_length=32, max_description_length=256, has_tags=False, **kwargs)
1200
+ self.type_: str = kwargs.get('type', 'URL List')
1201
+ if self.type_ not in self.valid_types:
1202
+ raise ValueError(f"Invalid type: {self.type_}. Must be one of: {', '.join(self.valid_types)}")
1203
+ self.member: Dict[List[str]] = kwargs.get('member', {'member': []})
1204
+
1205
+ @property
1206
+ def type_(self):
1207
+ return self._type_
1208
+
1209
+ @type_.setter
1210
+ def type_(self, value: str):
1211
+ if value not in self.valid_types:
1212
+ raise ValueError(f"Invalid type: {value}. Must be one of: {', '.join(self.valid_types)}")
1213
+ self._type_ = value
1214
+ self.entry.update({'type': value})
1215
+
1216
+ @property
1217
+ def member(self):
1218
+ return self._member
1219
+
1220
+ @member.setter
1221
+ def member(self, value: dict):
1222
+ if not isinstance(value, dict) or 'member' not in value or not isinstance(value['member'], list):
1223
+ raise TypeError("Member must be a dictionary with a 'member' key containing a list of strings.")
1224
+ for member in value['member']:
1225
+ if not isinstance(member, str):
1226
+ raise ValueError("Each member in the list must be a string.")
1227
+ self._member = value
1228
+ self.entry.update(**value)
1229
+
1230
+ def add_member(self, new_member: str):
1231
+ """Add a new member to the member list."""
1232
+ if not isinstance(new_member, str):
1233
+ raise ValueError("New member must be a string.")
1234
+
1235
+ # Ensure the member is not already in the list to avoid duplicates
1236
+ if new_member not in self._member['member']:
1237
+ self._member['member'].append(new_member)
1238
+ self.entry.update({'member': self._member['member']})
1239
+ else:
1240
+ print(f"Member {new_member} is already in the list.")
1241
+
1242
+ def remove_member(self, member_to_remove: str):
1243
+ """Remove an existing member from the member list."""
1244
+ if not isinstance(member_to_remove, str):
1245
+ raise ValueError("Member to remove must be a string.")
1246
+
1247
+ # Check if the member exists in the list before attempting to remove
1248
+ if member_to_remove in self._member['member']:
1249
+ self._member['member'].remove(member_to_remove)
1250
+ self.entry.update({'member': self._member['member']})
1251
+ else:
1252
+ print(f"Member {member_to_remove} not found in the list.")
1253
+
1254
+ class SDWANPathQualityProfiles(Object):
1255
+ def __init__(self, PANDevice, **kwargs):
1256
+ super().__init__(PANDevice, max_name_length=32, has_tags=False, **kwargs)
1257
+ self.metric: Dict = kwargs.get('metric')
1258
+
1259
+ @property
1260
+ def metric(self) -> Dict:
1261
+ return self._metric
1262
+
1263
+ @metric.setter
1264
+ def metric(self, value: Dict) -> None:
1265
+ if not isinstance(value, dict):
1266
+ raise TypeError("Metric must be a dictionary.")
1267
+
1268
+ valid_sensitivity = ['low', 'medium', 'high']
1269
+
1270
+ # Validate latency
1271
+ if 'latency' not in value or not isinstance(value['latency'], dict):
1272
+ raise ValueError("Latency metrics are missing or incorrect format.")
1273
+ latency_threshold = value['latency'].get('threshold', 100)
1274
+ latency_sensitivity = value['latency'].get('sensitivity', 'medium')
1275
+ if not (10 <= latency_threshold <= 3000):
1276
+ raise ValueError("Latency threshold must be between 10 and 3000.")
1277
+ if latency_sensitivity not in valid_sensitivity:
1278
+ raise ValueError("Invalid latency sensitivity value.")
1279
+
1280
+ # Validate pkt-loss
1281
+ if 'pkt-loss' not in value or not isinstance(value['pkt-loss'], dict):
1282
+ raise ValueError("Packet loss metrics are missing or incorrect format.")
1283
+ pkt_loss_threshold = value['pkt-loss'].get('threshold', 1)
1284
+ pkt_loss_sensitivity = value['pkt-loss'].get('sensitivity', 'medium')
1285
+ if not (1 <= pkt_loss_threshold <= 100):
1286
+ raise ValueError("Packet loss threshold must be between 1 and 100.")
1287
+ if pkt_loss_sensitivity not in valid_sensitivity:
1288
+ raise ValueError("Invalid packet loss sensitivity value.")
1289
+
1290
+ # Validate jitter
1291
+ if 'jitter' not in value or not isinstance(value['jitter'], dict):
1292
+ raise ValueError("Jitter metrics are missing or incorrect format.")
1293
+ jitter_threshold = value['jitter'].get('threshold', 100)
1294
+ jitter_sensitivity = value['jitter'].get('sensitivity', 'medium')
1295
+ if not (10 <= jitter_threshold <= 2000):
1296
+ raise ValueError("Jitter threshold must be between 10 and 2000.")
1297
+ if jitter_sensitivity not in valid_sensitivity:
1298
+ raise ValueError("Invalid jitter sensitivity value.")
1299
+
1300
+ # If all validations pass, set the metric
1301
+ self._metric = {
1302
+ 'latency': {'threshold': latency_threshold, 'sensitivity': latency_sensitivity},
1303
+ 'pkt-loss': {'threshold': pkt_loss_threshold, 'sensitivity': pkt_loss_sensitivity},
1304
+ 'jitter': {'threshold': jitter_threshold, 'sensitivity': jitter_sensitivity},
1305
+ }
1306
+ self.entry.update({'metric': self._metric})
1307
+
1308
+ class GlobalProtectHIPObjects(Object):
1309
+ def __init__(self, PANDevice, **kwargs):
1310
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1311
+
1312
+ class GlobalProtectHIPProfiles(Object):
1313
+ def __init__(self, PANDevice, **kwargs):
1314
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1315
+
1316
+ class CustomDataPatterns(Object):
1317
+ def __init__(self, PANDevice, **kwargs):
1318
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1319
+
1320
+ class CustomSpywareSignatures(Object):
1321
+ def __init__(self, PANDevice, **kwargs):
1322
+ super().__init__(PANDevice, has_tags=False, **kwargs)
1323
+ self.name: int = kwargs.get('name')
1324
+
1325
+ @property
1326
+ def name(self):
1327
+ return self._name
1328
+
1329
+ @name.setter
1330
+ def name(self, value: int) -> None:
1331
+ if 15000 <= value <= 18000 or 6900001 <= value <= 7000000:
1332
+ self._name = value
1333
+ self.entry.update({'name': value})
1334
+ else:
1335
+ raise ValueError("Value must be between 15000-18000 or 6900001-7000000")
1336
+
1337
+ class CustomVulnerabilitySignatures(Object):
1338
+ def __init__(self, PANDevice, **kwargs):
1339
+ super().__init__(PANDevice, has_tags=False, **kwargs)
1340
+ self.name: int = kwargs.get('name')
1341
+
1342
+ @property
1343
+ def name(self):
1344
+ return self._name
1345
+
1346
+ @name.setter
1347
+ def name(self, value: int) -> None:
1348
+ if 41000 <= value <= 45000 or 6800001 <= value <= 6900000:
1349
+ self._name = value
1350
+ self.entry.update({'name': value})
1351
+ else:
1352
+ raise ValueError("Value must be between 41000-45000 or 6800001-6900000")
1353
+
1354
+ class AntivirusSecurityProfiles(Object):
1355
+ def __init__(self, PANDevice, **kwargs):
1356
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1357
+
1358
+ class AntiSpywareSecurityProfiles(Object):
1359
+ def __init__(self, PANDevice, **kwargs):
1360
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1361
+
1362
+ class VulnerabilityProtectionSecurityProfiles(Object):
1363
+ def __init__(self, PANDevice, **kwargs):
1364
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1365
+
1366
+ class URLFilteringSecurityProfiles(Object):
1367
+ def __init__(self, PANDevice, **kwargs):
1368
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1369
+
1370
+ class FileBlockingSecurityProfiles(Object):
1371
+ def __init__(self, PANDevice, **kwargs):
1372
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1373
+
1374
+ class WildFireAnalysisSecurityProfiles(Object):
1375
+ def __init__(self, PANDevice, **kwargs):
1376
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1377
+
1378
+ class DataFilteringSecurityProfiles(Object):
1379
+ def __init__(self, PANDevice, **kwargs):
1380
+ super().__init__(PANDevice, max_name_length=65, max_description_lenght=255, has_tags=False, **kwargs)
1381
+
1382
+ class DoSProtectionSecurityProfiles(Object):
1383
+ def __init__(self, PANDevice, **kwargs):
1384
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1385
+
1386
+ class GTPProtectionSecurityProfiles(Object):
1387
+ def __init__(self, PANDevice, **kwargs):
1388
+ super().__init__(PANDevice, max_name_length=64, max_description_lenght=128, has_tags=False, **kwargs)
1389
+
1390
+ class SCTPProtectionSecurityProfiles(Object):
1391
+ def __init__(self, PANDevice, **kwargs):
1392
+ super().__init__(PANDevice, max_name_length=21, max_description_lenght=255, has_tags=False, **kwargs)
1393
+
1394
+ class SecurityProfileGroups(Object):
1395
+ def __init__(self, PANDevice, **kwargs):
1396
+ super().__init__(PANDevice, max_name_length=32, has_tags=False, **kwargs)
1397
+
1398
+ class LogForwardingProfiles(Object):
1399
+ def __init__(self, PANDevice, **kwargs):
1400
+ super().__init__(PANDevice, max_name_length=65, max_description_lenght=1024, has_tags=False, **kwargs)
1401
+
1402
+ class AuthenticationEnforcements(Object):
1403
+ def __init__(self, PANDevice, **kwargs):
1404
+ super().__init__(PANDevice, max_name_length=32, has_tags=False, **kwargs)
1405
+
1406
+ class DecryptionProfiles(Object):
1407
+ def __init__(self, PANDevice, **kwargs):
1408
+ super().__init__(PANDevice, max_name_length=32, has_tags=False, **kwargs)
1409
+
1410
+ class PacketBrokerProfiles(Object):
1411
+ def __init__(self, PANDevice, **kwargs):
1412
+ super().__init__(PANDevice, max_name_length=32, max_description_lenght=255, has_tags=False, **kwargs)
1413
+
1414
+ class SDWANSaasQualityProfiles(Object):
1415
+ def __init__(self, PANDevice, **kwargs):
1416
+ super().__init__(PANDevice, max_name_length=32, has_tags=False, **kwargs)
1417
+
1418
+ class SDWANTrafficDistributionProfiles(Object):
1419
+ def __init__(self, PANDevice, **kwargs):
1420
+ super().__init__(PANDevice, max_name_length=32, has_tags=False, **kwargs)
1421
+
1422
+ class SDWANErrorCorrectionProfiles(Object):
1423
+ def __init__(self, PANDevice, **kwargs):
1424
+ super().__init__(PANDevice, max_name_length=32, has_tags=False, **kwargs)
1425
+
1426
+ class Schedules(Object):
1427
+ def __init__(self, PANDevice, **kwargs):
1428
+ super().__init__(PANDevice, max_name_length=32, has_tags=False, **kwargs)