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/XDR.py ADDED
@@ -0,0 +1,299 @@
1
+ import requests
2
+ from datetime import datetime, timezone
3
+ import secrets
4
+ import string
5
+ import hashlib
6
+ import json
7
+ from requests.packages.urllib3.exceptions import InsecureRequestWarning
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ # Disable insecure request warnings
13
+ requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
14
+
15
+ class Cortex:
16
+ """
17
+ This class is used to establish a connection with a Cortex XDR tenant.
18
+ It generates the necessary headers for authentication.
19
+ """
20
+ def __init__(self, api_key_id, api_key, base_url, api_version='/public_api/v1'):
21
+ self.api_key_id = api_key_id
22
+ self.api_key = api_key
23
+ self.base_url = base_url
24
+ self.api_version = api_version
25
+
26
+ def generate_header(self):
27
+ """
28
+ Generates the authentication header required for Cortex XDR API requests.
29
+ """
30
+ nonce = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(64))
31
+ timestamp = int(datetime.now(timezone.utc).timestamp()) * 1000
32
+ auth_key = f"{self.api_key}{nonce}{timestamp}".encode("utf-8")
33
+ api_key_hash = hashlib.sha256(auth_key).hexdigest()
34
+
35
+ return {
36
+ "x-xdr-timestamp": str(timestamp),
37
+ "x-xdr-nonce": nonce,
38
+ "x-xdr-auth-id": str(self.api_key_id),
39
+ "Authorization": api_key_hash
40
+ }
41
+
42
+ class XDR_Base:
43
+ """
44
+ Base class for XDR interactions. This class initializes a connection session
45
+ and defines basic GET method functionality for the derived classes.
46
+ """
47
+ def __init__(self, cortex_instance, api_name='', **kwargs):
48
+ self.cortex_instance = cortex_instance
49
+ self.api_name = api_name
50
+ self.session = requests.Session()
51
+ self.call_name = kwargs.get('call_name')
52
+ self.params = kwargs.get('params')
53
+
54
+ def get(self):
55
+ """
56
+ Generic GET request to retrieve data from Cortex XDR API.
57
+ """
58
+ url = f"{self.cortex_instance.base_url}{self.cortex_instance.api_version}{self.api_name}{self.call_name}"
59
+ response = self.session.post(url, headers=self.cortex_instance.generate_header(), json=self.params)
60
+ return json.loads(response.text) if response.status_code == 200 else response.status_code
61
+
62
+ def status(self):
63
+ self.api_name = '/healthcheck/'
64
+ self.call_name = ''
65
+ return self.get().get('status')
66
+
67
+ def info(self):
68
+ self.api_name = '/system/'
69
+ self.call_name = 'get_tenant_info'
70
+ self.params = {'request_data': {}}
71
+
72
+ return self.get()
73
+ class Incidents(XDR_Base):
74
+ """
75
+ Class to handle incidents-related interactions with Cortex XDR.
76
+ """
77
+ def __init__(self, cortex_instance, **kwargs):
78
+ super().__init__(cortex_instance, '/incidents', **kwargs)
79
+
80
+ self.call_name = ''
81
+ self.params = kwargs.get('params') or {}
82
+ self.filter = []
83
+ self.ALERTFILTER = []
84
+ self.SEARCH_FROM = kwargs.get('SEARCH_FROM') or 0
85
+ self.SEARCH_TO = kwargs.get('SEARCH_TO') or 100
86
+ self.SORT_FIELD = kwargs.get('SORT_FIELD')
87
+ self.KEYWORD = kwargs.get('KEYWORD') or 'asc'
88
+
89
+ def IncidentFilterBuilder(self, FIELD, OPERATOR, VALUELIST):
90
+ Valid_FilterFields = ['modification_time', 'creation_time', 'incident_id_list', 'description', 'alert_sources',
91
+ 'status', 'starred']
92
+ Valid_FilterOperators = ['in', 'gte', 'lte', 'eq', 'neq', 'contains']
93
+ Valid_status = ['new', 'under_investigation', 'resolved_true_positive', 'resolved_known_issue',
94
+ 'resolved_duplicate_incident', 'resolved_false_positive', 'resolved_auto_resolve']
95
+
96
+ if FIELD not in Valid_FilterFields:
97
+ raise ValueError(f'{FIELD} is not a valid Field value. Please use one of {Valid_FilterFields}')
98
+
99
+ if OPERATOR not in Valid_FilterOperators:
100
+ raise ValueError(f'{OPERATOR} is not a valid Operator value. Please use one of {Valid_FilterOperators}')
101
+
102
+ if OPERATOR == 'in':
103
+ if FIELD in ['incident_id_list', 'alert_sources', 'description']:
104
+ if not isinstance(VALUELIST, list):
105
+ raise ValueError(f'{VALUELIST} must be a list')
106
+ elif OPERATOR == 'contains':
107
+ if FIELD not in ['description']:
108
+ raise ValueError(f'When using Operator {OPERATOR}, the only valid keywords are [description].')
109
+ elif OPERATOR in ['gte', 'lte']:
110
+ if FIELD not in ['modification_time', 'creation_time']:
111
+ raise ValueError(
112
+ f'When using Operator {OPERATOR}, the only valid keywords are [modification_time, creation_time].')
113
+ elif OPERATOR in ['eq', 'neq']:
114
+ if FIELD not in ['status']:
115
+ raise ValueError(f'When using Operator {OPERATOR}, the only valid keywords are [status].')
116
+ for i in VALUELIST:
117
+ if i not in Valid_status:
118
+ raise ValueError(f'Status must be one of {Valid_status}.')
119
+
120
+ self.filter.append({'field': FIELD, 'operator': OPERATOR, 'value': VALUELIST})
121
+
122
+ def AlertFilterBuilder(self, FIELD, OPERATOR, VALUELIST):
123
+ Valid_FilterFields = ['alert_id_list', 'alert_source', 'severity', 'creation_time', 'server_creation_time']
124
+ Valid_FilterOperators = ['in', 'gte', 'lte']
125
+ Valid_severity = ['low', 'medium', 'high', 'critical', 'informational']
126
+
127
+ if FIELD not in Valid_FilterFields:
128
+ raise ValueError(f'{FIELD} is not a valid Field value. Please use one of {Valid_FilterFields}')
129
+
130
+ if OPERATOR not in Valid_FilterOperators:
131
+ raise ValueError(f'{OPERATOR} is not a valid Operator value. Please use one of {Valid_FilterOperators}')
132
+
133
+ if OPERATOR == 'in':
134
+ if FIELD == 'alert_id':
135
+ if not isinstance(FIELD, list):
136
+ raise ValueError(f'{FIELD} must be a list')
137
+ elif FIELD == 'severity':
138
+ for i in VALUELIST:
139
+ if i not in Valid_severity:
140
+ raise ValueError(f'{FIELD} must be one of {Valid_severity} but {i} was provided.')
141
+ elif OPERATOR in ['gte', 'lte']:
142
+ if FIELD not in ['creation_time']:
143
+ raise ValueError(f'When using Operator {OPERATOR}, the only valid keywords are [creation_time].')
144
+ self.ALERTFILTER.append({'field': FIELD, 'operator': OPERATOR, 'value': VALUELIST})
145
+
146
+ def GetIncidents(self):
147
+ self.api_name = '/incidents'
148
+ self.call_name = '/get_incidents/'
149
+ self.SORT_FIELD = 'modification_time'
150
+ self.params = {
151
+ 'request_data': {
152
+ 'search_from': self.SEARCH_FROM,
153
+ 'search_to': self.SEARCH_TO,
154
+ 'sort': {
155
+ 'field': self.SORT_FIELD,
156
+ 'keyword': self.KEYWORD
157
+ },
158
+ 'filters': self.filter
159
+ }
160
+ }
161
+ return self.get()
162
+
163
+ def GetAlerts(self):
164
+ self.api_name = '/alerts'
165
+ self.call_name = '/get_alerts_multi_events/'
166
+ self.SORT_FIELD = 'severity'
167
+ self.params = {
168
+ 'request_data': {
169
+ 'search_from': self.SEARCH_FROM,
170
+ 'search_to': self.SEARCH_TO,
171
+ 'sort': {
172
+ 'field': self.SORT_FIELD,
173
+ 'keyword': self.KEYWORD
174
+ },
175
+ 'filters': self.ALERTFILTER
176
+ }
177
+ }
178
+ return self.get()
179
+
180
+ class Endpoint(XDR_Base):
181
+ """
182
+ Class to handle endpoint-related interactions with Cortex XDR.
183
+ """
184
+ def __init__(self, cortex_instance, **kwargs):
185
+ super().__init__(cortex_instance, '/endpoints', **kwargs)
186
+
187
+ self.call_name = ''
188
+ self.params = kwargs.get('params') or {}
189
+ self.filter = []
190
+ self.search_from = kwargs.get('search_from') or 0
191
+ self.search_to = kwargs.get('search_to') or 100
192
+ self.sort_field = kwargs.get('sort_field') or 'last_seen'
193
+ self.keyword = kwargs.get('keyword') or 'asc'
194
+
195
+ def get_all(self):
196
+ self.call_name = '/get_endpoints'
197
+ return self.get()
198
+
199
+ def filter_builder(self, field, operator, valuelist):
200
+ valid_filter_fields = ['endpoint_id_list', 'endpoint_status', 'dist_name', 'first_seen', 'last_seen', 'ip_list',
201
+ 'group_name', 'platform', 'alias', 'isolate', 'hostname']
202
+ valid_filter_operators = ['in', 'gte', 'lte']
203
+
204
+ valid_endpoint_status = ['connected', 'disconnected', 'lost', 'uninstalled']
205
+ valid_platform = ['windows', 'linux', 'macos', 'android']
206
+ valid_isolate = ['isolated', 'unisolated']
207
+ valid_scan_status = ['none', 'pending', 'in_progress', 'canceled', 'aborted', 'pending_cancellation',
208
+ 'success', 'error']
209
+
210
+ if field not in valid_filter_fields:
211
+ raise ValueError(f'{field} is not a valid Field value. Please use one of {valid_filter_fields}')
212
+
213
+ if operator not in valid_filter_operators:
214
+ raise ValueError(f'{operator} is not a valid Operator value. Please use one of {valid_filter_operators}')
215
+
216
+ if operator == 'in':
217
+ if field in ['endpoint_id_list', 'dist_name', 'group_name', 'alias', 'hostname', 'username']:
218
+ for i in valuelist:
219
+ if not isinstance(i, str):
220
+ raise ValueError(f'{i} must be a string')
221
+
222
+ elif field == 'endpoint_status':
223
+ for i in valuelist:
224
+ if i not in valid_endpoint_status:
225
+ raise ValueError(f'{i} must be one of {valid_endpoint_status}')
226
+ elif field == 'ip_list':
227
+ for i in valuelist:
228
+ if not isinstance(i, str):
229
+ # TODO - validate the string is an IP address
230
+ raise ValueError(f'{i} must be a string')
231
+ elif field == 'platform':
232
+ for i in valuelist:
233
+ if i not in valid_platform:
234
+ raise ValueError(f'{i} must be one of {valid_platform}')
235
+ elif field == 'isolate':
236
+ for i in valuelist:
237
+ if i not in valid_isolate:
238
+ raise ValueError(f'{i} must be one of {valid_isolate}')
239
+ elif field == 'scan_status':
240
+ for i in valuelist:
241
+ if i not in valid_scan_status:
242
+ raise ValueError(f'{i} must be one of {valid_scan_status}')
243
+ else:
244
+ if field not in ['first_seen', 'last_seen']:
245
+ raise ValueError(
246
+ f'When using Operator {operator}, the only valid keywords are [first_seen, last_seen].')
247
+
248
+ self.filter.append({'field': field, 'operator': operator, 'value': valuelist})
249
+
250
+ def get_endpoint(self):
251
+ self.call_name = '/get_endpoint'
252
+ self.params = {
253
+ 'request_data': {
254
+ 'search_from': self.search_from,
255
+ 'search_to': self.search_to,
256
+ 'sort': {
257
+ 'field': self.sort_field,
258
+ 'keyword': self.keyword
259
+ },
260
+ 'filters': self.filter
261
+ }
262
+ }
263
+ return self.get()
264
+
265
+ def get_violations(self):
266
+ self.api_name = '/device_control'
267
+ self.call_name = '/get_violations'
268
+ self.params = {
269
+ 'request_data': {
270
+ 'search_from': self.search_from,
271
+ 'search_to': self.search_to,
272
+ 'sort': {
273
+ 'field': self.sort_field,
274
+ 'keyword': self.keyword
275
+ },
276
+ 'filters': self.filter
277
+ }
278
+ }
279
+ return self.get()
280
+
281
+ def raw_date(date, format):
282
+ """
283
+ Taka a date that has a suffix of th, st or rd on a number and remove it.
284
+ :param date:
285
+ :return: The date in unixtime
286
+ """
287
+ BadDateList = date.split(' ')
288
+ newDate = []
289
+ for i in BadDateList:
290
+ if i.endswith('th'):
291
+ newDate.append(i.strip('th'))
292
+ elif i.endswith('st'):
293
+ newDate.append(i.strip('st'))
294
+ elif i.endswith('rd'):
295
+ newDate.append(i.strip('rd'))
296
+ else:
297
+ newDate.append(i)
298
+ GoodDate = datetime.strptime(' '.join(newDate), format)
299
+ return int(GoodDate.timestamp()) * 1000
@@ -0,0 +1,4 @@
1
+
2
+
3
+
4
+
@@ -0,0 +1,209 @@
1
+ Metadata-Version: 2.1
2
+ Name: PyPANRestV2
3
+ Version: 2.1.0
4
+ Summary: Python tools for interacting with Palo Alto Networks REST API.
5
+ License: MIT
6
+ Author: Mark Rzepa
7
+ Author-email: mark@rzepa.com
8
+ Requires-Python: >=3.11
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Requires-Dist: dnspython (>=2.6.1)
14
+ Requires-Dist: icecream (>=2.1.3)
15
+ Requires-Dist: pycountry (>=23.12.11)
16
+ Requires-Dist: python-dotenv (>=1.0.1)
17
+ Requires-Dist: requests (>=2.31.0)
18
+ Requires-Dist: tqdm (>=4.66.2)
19
+ Requires-Dist: validators (>=0.22.0)
20
+ Requires-Dist: xmltodict (>=0.13.0)
21
+ Description-Content-Type: text/markdown
22
+
23
+ # PyPanRestV2
24
+
25
+ **PyPanRestV2** is a Python library designed to simplify interactions with Palo Alto Networks firewalls and Panorama via their REST API. It provides a higher level of abstraction, allowing users to manage firewalls and Panorama without needing to construct REST requests manually or work with XML for areas of the firewall configuration that still require it.
26
+
27
+ ---
28
+
29
+ ## Features
30
+
31
+ - **High-Level Abstraction**: Simplifies interaction with the Palo Alto Networks API.
32
+ - **Support for Firewalls and Panorama**: Manage both individual firewalls and Panorama devices.
33
+ - **REST API Integration**: Allows seamless communication with devices.
34
+ - **Convenient Pythonic Objects**: Intuitive Python objects for interacting with specific sections of Palo Alto firewall configurations.
35
+
36
+ ---
37
+
38
+ ## Installation
39
+
40
+ To install `PyPanRestV2`, clone the repository and install it as a package:
41
+
42
+ ```bash
43
+ # Clone the repository
44
+ git clone https://github.com/mrzepa/pypanrestv2.git
45
+
46
+ # Navigate to the project directory
47
+ cd pypanrestv2
48
+
49
+ # Install the package in development mode
50
+ pip install -e .
51
+ ```
52
+
53
+ This will install the package and all required dependencies automatically. The `-e` flag installs the package in "editable" mode, which is useful if you plan to modify the code or contribute to the project.
54
+
55
+ ---
56
+
57
+ ## Basic Usage
58
+
59
+ ### Import the Required Classes
60
+ Start by importing the necessary classes from the library:
61
+
62
+ ```python
63
+ from pypantrestv2 import Firewall, Panorama
64
+ ```
65
+
66
+ ### Connect to a Firewall or Panorama Device
67
+ Create a `Firewall` or `Panorama` object by providing the required connection details:
68
+
69
+ For a **Firewall**:
70
+ ```python
71
+ firewall = Firewall(base_url="192.168.1.1", api_key="12345")
72
+ ```
73
+
74
+ For **Panorama**:
75
+ ```python
76
+ panorama = Panorama(base_url="192.168.2.1", username="admin", password="my_password")
77
+ ```
78
+
79
+ ### Common Use Cases
80
+
81
+ #### 1. Managing Security Rules
82
+ ```python
83
+ from pypanrestv2.Policies import SecurityRules
84
+
85
+ # Create a new security rule
86
+ security_rule = SecurityRules(firewall, name='allow_web')
87
+ security_rule.source_zone = ['trust']
88
+ security_rule.destination_zone = ['untrust']
89
+ security_rule.source = ['any']
90
+ security_rule.destination = ['any']
91
+ security_rule.application = ['web-browsing']
92
+ security_rule.service = ['application-default']
93
+ security_rule.action = 'allow'
94
+ security_rule.create()
95
+
96
+ # Modify an existing rule
97
+ existing_rule = SecurityRules(firewall, name='existing_rule')
98
+ existing_rule.refresh() # Load current configuration
99
+ existing_rule.action = 'deny'
100
+ existing_rule.update()
101
+ ```
102
+
103
+ #### 2. Managing Address Objects
104
+ ```python
105
+ from pypanrestv2.Objects import Addresses
106
+
107
+ # Create a new address object
108
+ address = Addresses(firewall, name='web_server')
109
+ address.value = '192.168.1.100'
110
+ address.type = 'ip-netmask'
111
+ address.create()
112
+
113
+ # Get all address objects
114
+ all_addresses = Addresses.get_all(firewall)
115
+ ```
116
+
117
+ #### 3. Working with Panorama Device Groups
118
+ ```python
119
+ from pypanrestv2 import Panorama
120
+
121
+ # Initialize Panorama connection
122
+ panorama = Panorama(base_url='panorama.example.com', api_key='YOUR_API_KEY')
123
+
124
+ # Add a device to a device group
125
+ device_group = panorama.device_groups.get('Branch_Offices')
126
+ device_group.add_device('serial123')
127
+ ```
128
+
129
+ ---
130
+
131
+ ## Repository
132
+
133
+ Visit the project's GitHub repository for source code, documentation, enhancements, and contributions:
134
+
135
+ [PyPanRestV2 Repository on GitHub](https://github.com/wellhealthtechnologies/PyPanRestV2.git)
136
+
137
+ ---
138
+
139
+ ## Requirements
140
+
141
+ - **Python 3.11+** (or higher)
142
+ - **Palo Alto Devices** or Panorama
143
+ - Python modules listed in requirements.txt
144
+
145
+ ---
146
+
147
+ ## API Documentation
148
+
149
+ The SDK provides access to the following main components:
150
+
151
+ ### Core Modules
152
+ - `Firewall/Panorama`: Base connection and authentication
153
+ - `Policies`: Security rules, NAT rules, and policy management
154
+ - `Objects`: Address objects, service objects, and security profiles
155
+ - `Network`: Interfaces, zones, and routing configuration
156
+ - `Device`: System settings and device management
157
+
158
+ ### Error Handling
159
+
160
+ The SDK uses custom exceptions for better error handling:
161
+
162
+ ```python
163
+ from pypanrestv2.Exceptions import PANConnectionError, PANConfigError
164
+
165
+ try:
166
+ firewall = Firewall(base_url='192.168.1.1', api_key='invalid_key')
167
+ firewall.test_connection()
168
+ except PANConnectionError as e:
169
+ print(f'Connection failed: {e}')
170
+ except PANConfigError as e:
171
+ print(f'Configuration error: {e}')
172
+ ```
173
+
174
+ Common errors and solutions:
175
+ - `PANConnectionError`: Check network connectivity and API credentials
176
+ - `PANConfigError`: Verify object names and configuration values
177
+ - `PANNotFoundError`: Ensure referenced objects exist
178
+
179
+ ## Status and Updates
180
+
181
+ This SDK is actively maintained and regularly updated to support new PAN-OS versions. While not all API endpoints are implemented, core functionality is stable and production-ready. Check the GitHub repository for the latest updates and supported features.
182
+
183
+ ## Contributing
184
+
185
+ Contributions are welcome! If you want to report issues, request features, or contribute to the library:
186
+
187
+ 1. Fork the repository.
188
+ 2. Create a feature branch: `git checkout -b my-feature`.
189
+ 3. Commit your changes: `git commit -m "Add detailed description of changes"`.
190
+ 4. Push to the branch: `git push origin my-feature`.
191
+ 5. Submit a pull request.
192
+
193
+ Be sure to check the documentation, if provided, before starting contributions.
194
+
195
+ ---
196
+
197
+ ## License
198
+
199
+ This project is licensed under the MIT. See the [LICENSE](./https://opensource.org/license/mit) file for details.
200
+
201
+ ---
202
+
203
+ ## Author
204
+
205
+ Mark Rzepa
206
+ mark@rzepa.com
207
+
208
+ ---
209
+
@@ -0,0 +1,13 @@
1
+ pypanrestv2/ApplicationHelper.py,sha256=fRszLlhaJevcECA_fiiDg9CW7bqXm8-1GDThwddVm7k,8486
2
+ pypanrestv2/Base.py,sha256=XIN319xoaMGauI8yqiMmXTzNNcPqT4glAlrkiQPvWes,78763
3
+ pypanrestv2/Device.py,sha256=wvjJlRu07WNZd-QgPIKhl5BX0MRWDQO-FACjwdYScb8,617
4
+ pypanrestv2/Exceptions.py,sha256=jrhXq1JkIZz8mCZ8DViZwx_TThpT-dN5cxsDbMfY-wI,397
5
+ pypanrestv2/Network.py,sha256=-thHyOf7XRQHtVkCTZfvQVQGr2O8d4VW_PEnYtGo2Mg,67894
6
+ pypanrestv2/Objects.py,sha256=zmwf1wkgS13rD66LmQzmwfSSL6LCG8_c-PUibF_G52c,62116
7
+ pypanrestv2/Panorama.py,sha256=8_uGEu236YORE8avXitqXUb9F5y-ujJl907O9XdoKgo,17877
8
+ pypanrestv2/Policies.py,sha256=rsNkYuaPpZQiG-Zqm_NiwrJkYTpceYWegSWq2YqlDkY,31844
9
+ pypanrestv2/XDR.py,sha256=K_am9ipuToadrSpY_C3yPRrQWlY4Xo_P-PeOxVWxL2Y,12386
10
+ pypanrestv2/__init__.py,sha256=VFw4sJIt4Zc0-__eYnksN8Ku9qMhbPpHJEkXMWUiD30,4
11
+ pypanrestv2-2.1.0.dist-info/METADATA,sha256=odLdfa36SEwfNptlCbfOtLVJk_ZmQbIgtz2RgyTLh9I,6191
12
+ pypanrestv2-2.1.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
13
+ pypanrestv2-2.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any