diff --git a/README.md b/README.md index a391348..d3ef89d 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,32 @@ # Python SDK for the Prisma Cloud APIs -This project includes a Python SDK for the Prisma Cloud APIs (CSPM, CWPP, and CCS) in the form of a Python package. +This is a Python SDK for the Prisma Cloud APIs (CSPM, CWPP, and PCCS) in the form of a Python package. + +This package is a fork of [prismacloud-api-python](https://github.com/PaloAltoNetworks/prismacloud-api-python), package [prismacloud-api](https://pypi.org/project/prismacloud-api), forked at version 5.2.24. + +This package is not maintained by Prisma Cloud SEs. + It also includes reference scripts that utilize this SDK. -Major changes with Version 5.0: -* Command-line argument and configuration file changes. ## Table of Contents -* [Setup](#Setup) +* [Installation](#Installation) * [Support](#Support) +* [References](#References) +* [Changelog](#Changelog) -## Setup +## Installation -Install the SDK via `pip3`: +Install the SDK via `pip`: ``` -pip3 install prismacloud-api +pip3 install prismacloudapi ``` -Please refer to [PyPI](https://pypi.org/project/prismacloud-api) for details. +Please refer to [PyPI](https://pypi.org/project/prismacloudapi) for details. ### Example Scripts @@ -101,5 +106,29 @@ settings = { ## Support -This project has been developed by members of the Prisma Cloud CS and SE teams, it is not Supported by Palo Alto Networks. -Nevertheless, the maintainers will make a best-effort to address issues, and (of course) contributors are encouraged to submit issues and pull requests. +This package is not maintained by Prisma Cloud SEs or any Palo Alto Networks employees. + +The maintainers will make a best-effort to address issues, and (of course) contributors are encouraged to submit issues and pull requests. + + +## References + +Prisma Cloud APIs: + +https://prisma.pan.dev/api/cloud/ + +Access Keys: + +https://docs.paloaltonetworks.com/prisma/prisma-cloud/prisma-cloud-admin/manage-prisma-cloud-administrators/create-access-keys.html + +Permissions: + +https://docs.paloaltonetworks.com/prisma/prisma-cloud/prisma-cloud-admin/manage-prisma-cloud-administrators/prisma-cloud-admin-permissions.html + +## Changelog + +2025-03 Major changes with Version 5.2.28: +* Leverage iterator construct for large dataset + +2024-01 Major changes with Version 5.0: +* Command-line argument and configuration file changes. diff --git a/prismacloud/api/README.md b/prismacloud/api/README.md deleted file mode 100644 index 671afde..0000000 --- a/prismacloud/api/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Python SDK for the Prisma Cloud APIs - -This is a Python SDK for the Prisma Cloud APIs (CSPM, CWPP, and PCCS) in the form of a Python package. - - -## Table of Contents - -* [Setup](#Usage) -* [Support](#Support) -* [References](#References) - - -## Setup - -Install the SDK via: - -``` -pip3 install prismacloud-api -``` - - -## Support - -This project has been developed by Prisma Cloud SEs, it is not Supported by Palo Alto Networks. -Nevertheless, the maintainers will make a best-effort to address issues, and (of course) contributors are encouraged to submit issues and pull requests. - - -### References - -Prisma Cloud APIs: - -https://prisma.pan.dev/api/cloud/ - -Access Keys: - -https://docs.paloaltonetworks.com/prisma/prisma-cloud/prisma-cloud-admin/manage-prisma-cloud-administrators/create-access-keys.html - -Permissions: - -https://docs.paloaltonetworks.com/prisma/prisma-cloud/prisma-cloud-admin/manage-prisma-cloud-administrators/prisma-cloud-admin-permissions.html \ No newline at end of file diff --git a/prismacloud/api/cspm/_endpoints.py b/prismacloud/api/cspm/_endpoints.py deleted file mode 100644 index 8aa6402..0000000 --- a/prismacloud/api/cspm/_endpoints.py +++ /dev/null @@ -1,705 +0,0 @@ -""" Prisma Cloud API Endpoints Class """ - -# TODO: Split into multiple files, one per endpoint ... - -# pylint: disable=too-many-public-methods - - -class EndpointsPrismaCloudAPIMixin(): - """ Prisma Cloud API Endpoints Class """ - - def current_user(self): - return self.execute('GET', 'user/me') - - """ - Note: Eventually, all objects covered should have full CRUD capability, ie, to create, read, update, and delete (and list). - - Template - - [ ] LIST - [ ] CREATE - [ ] READ - [ ] UPDATE - [ ] DELETE - """ - - """ - Alerts - - [x] LIST - [ ] CREATE - [ ] READ - [ ] UPDATE - [ ] DELETE - Additional: - [x] LIST (v2) - """ - - def alert_list_read(self, query_params=None, body_params=None): - return self.execute('POST', 'alert', query_params=query_params, body_params=body_params) - - def alert_v2_list_read(self, query_params=None, body_params=None): - return self.execute('POST', 'v2/alert', query_params=query_params, body_params=body_params, paginated=True) - - def alert_csv_create(self, body_params=None): - return self.execute('POST', 'alert/csv', body_params=body_params) - - def alert_csv_status(self, csv_report_id): - return self.execute('GET', 'alert/csv/%s/status' % csv_report_id) - - def alert_csv_download(self, csv_report_id): - return self.execute('GET', 'alert/csv/%s/download' % csv_report_id) - - - """ - Policies - - [x] LIST - [x] CREATE - [x] READ - [x] UPDATE - [x] DELETE - Additional: - [x] LIST (v2) - [x] LIST (v2) CUSTOM - [x] UPDATE STATUS - """ - - def policy_list_read(self): - return self.execute('GET', 'policy') - - def policy_v2_list_read(self): - return self.execute('GET', 'v2/policy') - - def policy_custom_v2_list_read(self): - filters = [('policy.policyMode', 'custom')] - return self.execute('GET', 'v2/policy', query_params=filters) - - def policy_create(self, policy_to_add): - return self.execute('POST', 'policy', body_params=policy_to_add) - - def policy_read(self, policy_id, message=None): - self.progress(message) - return self.execute('GET', 'policy/%s' % policy_id) - - def policy_update(self, policy_id, policy_update): - return self.execute('PUT', 'policy/%s' % policy_id, body_params=policy_update) - - def policy_status_update(self, policy_id, policy_status_update): - return self.execute('PATCH', 'policy/%s/status/%s' % (policy_id, policy_status_update)) - - def policy_delete(self, policy_id): - return self.execute('DELETE', 'policy/%s' % policy_id) - - """ - Saved Searches - - [x] LIST - [x] CREATE - [x] READ - [ ] UPDATE - [x] DELETE - """ - - def saved_search_list_read(self): - return self.execute('GET', 'search/history?filter=saved') - - def saved_search_create(self, type_of_search, saved_search_to_add): - if type_of_search == 'network': - return self.execute('POST', 'search', body_params=saved_search_to_add) - if type_of_search == 'audit_event': - return self.execute('POST', 'search/event', body_params=saved_search_to_add) - return self.execute('POST', 'search/%s' % type_of_search, body_params=saved_search_to_add) - - def saved_search_read(self, saved_search_id, message=None): - self.progress(message) - return self.execute('GET', 'search/history/%s' % saved_search_id) - - def saved_search_delete(self, saved_search_id): - return self.execute('DELETE', 'search/history/%s' % saved_search_id) - - """ - Compliance Standards - - [x] LIST - [x] CREATE - [x] READ - [ ] UPDATE - [x] DELETE - """ - - def compliance_standard_list_read(self): - return self.execute('GET', 'compliance') - - def compliance_standard_create(self, compliance_standard_to_add): - return self.execute('POST', 'compliance', body_params=compliance_standard_to_add) - - def compliance_standard_read(self, compliance_standard_id): - return self.execute('GET', 'compliance/%s' % compliance_standard_id) - - def compliance_standard_delete(self, compliance_standard_id): - return self.execute('DELETE', 'compliance/%s' % compliance_standard_id) - - """ - Compliance Standard Requirements - - [x] LIST - [x] CREATE - [ ] READ - [ ] UPDATE - [ ] DELETE - """ - - def compliance_standard_requirement_list_read(self, compliance_standard_id): - return self.execute('GET', 'compliance/%s/requirement' % compliance_standard_id) - - def compliance_standard_requirement_create(self, compliance_standard_id, compliance_requirement_to_add): - return self.execute('POST', 'compliance/%s/requirement' % compliance_standard_id, body_params=compliance_requirement_to_add) - - """ - Compliance Standard Requirements Sections - - [x] LIST - [x] CREATE - [ ] READ - [ ] UPDATE - [ ] DELETE - """ - - def compliance_standard_requirement_section_list_read(self, compliance_requirement_id): - return self.execute('GET', 'compliance/%s/section' % compliance_requirement_id) - - def compliance_standard_requirement_section_create(self, compliance_requirement_id, compliance_section_to_add): - return self.execute('POST', 'compliance/%s/section' % compliance_requirement_id, body_params=compliance_section_to_add) - - """ - Compliance Standard Requirements Policies - - [x] LIST - [ ] CREATE - [ ] READ - [ ] UPDATE - [ ] DELETE - Additional: - [x] LIST (v2) - - """ - - def compliance_standard_policy_list_read(self, compliance_standard_name): - filters = [('policy.complianceStandard', compliance_standard_name)] - return self.execute('GET', 'policy', query_params=filters) - - def compliance_standard_policy_v2_list_read(self, compliance_standard_name): - filters = [('policy.complianceStandard', compliance_standard_name)] - return self.execute('GET', 'v2/policy', query_params=filters) - - """ - Users - - [x] LIST - [x] CREATE - [x] READ - [x] UPDATE - [x] DELETE - """ - - def user_list_read(self): - return self.execute('GET', 'v2/user') - - def user_create(self, user): - return self.execute('POST', 'v2/user', body_params=user) - - def user_read(self, user_id): - return self.execute('GET', 'v2/user/%s' % user_id) - - def user_update(self, user): - return self.execute('PUT', 'v2/user/%s' % user['email'], body_params=user) - - def user_delete(self, user_id): - return self.execute('DELETE', 'user/%s' % user_id) - - def user_bypass_sso(self, body_params): - return self.execute('PUT', 'user/saml/bypass', body_params=body_params) - - """ - User Roles - - [x] LIST - [ ] CREATE - [x] READ - [x] UPDATE - [x] DELETE - """ - - def user_role_list_read(self): - return self.execute('GET', 'user/role') - - def user_role_create(self, user_role_to_add): - return self.execute('POST', 'user/role', body_params=user_role_to_add) - - def user_role_update(self, user_role_id, user_role_update): - return self.execute('PUT', 'user/role/%s' % user_role_id, body_params=user_role_update) - - def user_role_read(self, user_role_id): - return self.execute('GET', 'user/role/%s' % user_role_id) - - def user_role_delete(self, user_role_id): - return self.execute('DELETE', 'user/role/%s' % user_role_id) - - """ - Access Keys - - [x] LIST - [x] CREATE - [x] READ - [x] UPDATE - [x] DELETE - Additional: - [x] UPDATE STATUS - """ - - def access_keys_list_read(self): - return self.execute('GET', 'access_keys') - - def access_key_create(self, access_key_to_add): - return self.execute('POST', 'access_keys', body_params=access_key_to_add) - - def access_key_read(self, access_key_id): - return self.execute('GET', 'access_keys/%s' % access_key_id) - - def access_key_update(self, access_key_id, access_key_update): - return self.execute('PUT', 'access_keys/%s' % access_key_id, body_params=access_key_update) - - # Note: Expired keys cannot be enabled. - def access_key_status_update(self, access_key_id, access_key_status): - return self.execute('PATCH', 'access_keys/%s/status/%s' % (access_key_id, access_key_status)) - - def access_key_delete(self, access_key_id): - return self.execute('DELETE', 'access_keys/%s' % access_key_id) - - """ - Cloud Accounts - - [x] LIST - [x] CREATE - [ ] READ - [x] UPDATE - [x] DELETE - Additional: - [x] LIST NAMES - """ - - def cloud_accounts_list_read(self, query_params=None): - return self.execute('GET', 'cloud', query_params=query_params) - - def cloud_accounts_children_list_read(self, cloud_account_type, cloud_account_id): - return self.execute('GET', 'cloud/%s/%s/project' % (cloud_account_type, cloud_account_id)) - - def cloud_accounts_list_names_read(self, query_params=None): - return self.execute('GET', 'cloud/name', query_params=query_params) - - def cloud_accounts_create(self, cloud_type, cloud_account_to_add): - return self.execute('POST', 'cloud/%s' % cloud_type, body_params=cloud_account_to_add) - - def cloud_account_info_read(self, cloud_type, cloud_account_id): - return self.execute('GET', 'cloud/%s/%s' % (cloud_type, cloud_account_id)) - - def cloud_account_update(self, cloud_type, cloud_account_id, cloud_account_update): - return self.execute('PUT', 'cloud/%s/%s' % (cloud_type, cloud_account_id), body_params=cloud_account_update) - - def cloud_account_delete(self, cloud_type, cloud_account_id): - return self.execute('DELETE', 'cloud/%s/%s' % (cloud_type, cloud_account_id)) - - def cloud_types_list_read(self, query_params=None): - return self.execute('GET', 'cloud/type', query_params=query_params) - - """ - Cloud Account Groups - - [x] LIST - [x] CREATE - [x] READ - [x] UPDATE - [x] DELETE - """ - - def cloud_account_group_list_read(self): - return self.execute('GET', 'cloud/group') - - def cloud_account_group_create(self, cloud_account_group_to_add): - return self.execute('POST', 'cloud/group', body_params=cloud_account_group_to_add) - - def cloud_account_group_read(self, cloud_account_group_id): - return self.execute('GET', 'cloud/group/%s' % cloud_account_group_id) - - def cloud_account_group_update(self, cloud_account_group_id, cloud_account_group_update): - return self.execute('PUT', 'cloud/group/%s' % cloud_account_group_id, body_params=cloud_account_group_update) - - def cloud_account_group_delete(self, cloud_account_group_id): - return self.execute('DELETE', 'cloud/group/%s' % cloud_account_group_id) - - """ - Asset (Resources) Inventory - - [x] LIST - [ ] CREATE - [ ] READ - [ ] UPDATE - [ ] DELETE - Additional: - [x] LIST (v2) - [x] LIST WITH FILTERS(v2) - """ - - def asset_inventory_list_read(self, query_params=None): - return self.execute('GET', 'v2/inventory', query_params=query_params) - - def asset_inventory_list_read_post(self, body_params=None): - return self.execute('POST', 'v2/inventory', body_params=body_params) - - """ - Asset (Resources) Inventory V3 - - [x] LIST - [ ] CREATE - [ ] READ - [ ] UPDATE - [ ] DELETE - Additional: - [x] LIST (v3) - [x] LIST WITH FILTERS(v3) - """ - - def asset_inventory_list_read_v3(self, query_params=None): - return self.execute('GET', 'v3/inventory', query_params=query_params) - - def asset_inventory_list_read_postv_3(self, body_params=None): - return self.execute('POST', 'v3/inventory', body_params=body_params) - - """ - (Assets) Resources - - [ ] LIST - [ ] CREATE - [x] READ - [ ] UPDATE - [ ] DELETE - """ - - def resource_read(self, body_params=None, force=False, message=None): - self.progress(message) - return self.execute('POST', 'resource', body_params=body_params, force=force) - - def resource_network_read(self, body_params=None, force=False): - return self.execute('POST', 'resource/network', body_params=body_params, force=force) - - def resource_scan_info_read(self, body_params=None): - result = [] - page_number = 1 - while page_number == 1 or 'pageToken' in body_params: - api_response = self.execute( - 'POST', 'resource/scan_info', body_params=body_params) - if 'resources' in api_response: - result.extend(api_response['resources']) - if 'nextPageToken' in api_response: - body_params['pageToken'] = api_response['nextPageToken'] - else: - body_params.pop('pageToken', None) - # if 'totalMatchedCount' in api_response: - # self.progress('Resources: %s, Page Size: %s, Page: %s' % (api_response['totalMatchedCount'], body_params['limit'], page_number)) - page_number += 1 - return result - - """ - Alert Rules - - [x] LIST - [x] CREATE - [x] READ - [x] UPDATE - [x] DELETE - Additional: - [x] LIST (v2) - """ - - def alert_rule_list_read(self): - return self.execute('GET', 'v2/alert/rule') - - def alert_rule_create(self, alert_rule): - return self.execute('POST', 'alert/rule', body_params=alert_rule) - - def alert_rule_read(self, alert_rule_id): - return self.execute('GET', 'alert/rule/%s' % alert_rule_id) - - def alert_rule_delete(self, alert_rule_id): - return self.execute('DELETE', 'alert/rule/%s' % alert_rule_id) - - def alert_rule_update(self, alert_rule_id, alert_rule_update): - return self.execute('PUT', 'alert/rule/%s' % alert_rule_id, body_params=alert_rule_update) - - """ - Integrations - - [x] LIST - [ ] CREATE - [ ] READ - [ ] UPDATE - [x] DELETE - Additional: - [ ] LIST (v2) - """ - - def integration_list_read(self): - return self.execute('GET', 'integration') - - def integration_delete(self, integration_id): - return self.execute('DELETE', 'integration/%s' % integration_id) - - """ - Resource Lists - - [x] LIST - [X] CREATE - [ ] READ - [ ] UPDATE - [x] DELETE - """ - - def resource_list_read(self): - return self.execute('GET', 'v1/resource_list') - - def resource_list_delete(self, resource_list_id): - return self.execute('DELETE', 'v1/resource_list/%s' % resource_list_id) - - def resource_list_create(self, resource_list_to_add): - return self.execute('POST', 'v1/resource_list', body_params=resource_list_to_add) - - """ - Adoption Advisor - - [x] LIST - [X] CREATE - [ ] READ - [X] UPDATE - [X] DELETE - """ - - def adoptionadvisor_report_read(self): - return self.execute('GET', 'adoptionadvisor/report') - - def adoptionadvisor_report_delete(self, report_id): - return self.execute('DELETE', 'adoptionadvisor/report/%s' % report_id) - - def adoptionadvisor_report_create(self, report_to_add): - return self.execute('POST', 'adoptionadvisor/report', body_params=report_to_add) - - """ - Compliance Reports - - [x] LIST - [x] CREATE - [ ] READ - [ ] UPDATE - [x] DELETE - Additional: - [x] DOWNLOAD - """ - - def compliance_report_list_read(self): - return self.execute('GET', 'report') - - def compliance_report_create(self, report_to_add): - return self.execute('POST', 'report', body_params=report_to_add) - - def compliance_report_delete(self, report_id): - return self.execute('DELETE', 'report/%s' % report_id) - - def compliance_report_download(self, report_id): - return self.execute('GET', 'report/%s/download' % report_id) - # TODO: - # if response_status == 204: - # # download pending - # pass - # elif response_status == 200: - # # download ready - # pass - # else: - - """ - Search - - [ ] LIST - [ ] CREATE - [x] READ - [ ] UPDATE - [ ] DELETE - """ - - def search_config_read(self, search_params): - result = [] - next_page_token = None - api_response = self.execute( - 'POST', 'search/config', body_params=search_params) - if 'data' in api_response and 'items' in api_response['data']: - result = api_response['data']['items'] - next_page_token = api_response['data'].pop('nextPageToken', None) - while next_page_token: - api_response = self.execute( - 'POST', 'search/config/page', body_params={'limit': 1000, 'pageToken': next_page_token, 'withResourceJson': 'true'}) - if 'items' in api_response: - result.extend(api_response['items']) - next_page_token = api_response.pop('nextPageToken', None) - return result - - def search_network_read(self, search_params, filtered=False): - search_url = 'search' - if filtered: - search_url = 'search/filtered' - return self.execute('POST', search_url, body_params=search_params) - - def search_event_read(self, search_params, subsearch=None): - result = [] - next_page_token = None - search_url = 'search/event' - if subsearch and subsearch in ['aggregate', 'filtered']: - search_url = 'search/event/%s' % subsearch - api_response = self.execute( - 'POST', search_url, body_params=search_params) - if 'data' in api_response and 'items' in api_response['data']: - result = api_response['data']['items'] - next_page_token = api_response['data'].pop('nextPageToken', None) - while next_page_token: - api_response = self.execute( - 'POST', 'search/config/page', body_params={'limit': 1000, 'pageToken': next_page_token}) - if 'items' in api_response: - result.extend(api_response['items']) - next_page_token = api_response.pop('nextPageToken', None) - return result - - def search_iam_read(self, search_params): - result = [] - next_page_token = None - api_response = self.execute( - 'POST', 'api/v1/permission', body_params=search_params) - if 'data' in api_response and 'items' in api_response['data']: - result = api_response['data']['items'] - next_page_token = api_response['data'].pop('nextPageToken', None) - while next_page_token: - api_response = self.execute( - 'POST', 'api/v1/permission/page', body_params={'limit': 1000, 'pageToken': next_page_token, 'withResourceJson': 'true'}) - if 'items' in api_response: - result.extend(api_response['items']) - next_page_token = api_response.pop('nextPageToken', None) - return result - - def search_iam_source_to_granter(self, search_params): - search_url = 'api/v1/permission/graph/source_to_granter' - return self.execute('POST', search_url, body_params=search_params) - - def search_iam_granter_to_dest(self, search_params): - search_url = 'api/v1/permission/graph/granter_to_dest' - return self.execute('POST', search_url, body_params=search_params) - - def search_suggest_list_read(self, query_to_suggest): - return self.execute('POST', 'search/suggest', body_params=query_to_suggest) - - """ - Configuration - - [ ] LIST - [ ] CREATE - [x] READ - [ ] UPDATE - [ ] DELETE - """ - - def compute_config(self): - return self.execute('GET', 'compute/config') - - def meta_info(self): - return self.execute('GET', 'meta_info') - - """ - Usage - - [ ] LIST - [ ] CREATE - [x] READ - [ ] UPDATE - [ ] DELETE - """ - - def resource_usage_by_cloud_type(self, body_params): - return self.execute('POST', 'license/api/v1/usage', body_params=body_params) - - def resource_usage_over_time(self, body_params): - return self.execute('POST', 'license/api/v1/usage/time_series', body_params=body_params) - - def resource_usage_by_cloud_type_v2(self, body_params): - return self.execute('POST', 'license/api/v2/usage', body_params=body_params) - - def resource_usage_over_time_v2(self, body_params): - return self.execute('POST', 'license/api/v2/time_series', body_params=body_params) - - """ - SSO SAML - - [X] LIST - [X] CREATE - [X] READ - [X] UPDATE - [ ] DELETE - """ - - def saml_config_read(self): - return self.execute('GET', 'authn/v1/saml/config') - - def saml_config_create(self, body_params): - return self.execute('POST', 'authn/v1/saml/config', body_params=body_params) - - def saml_config_update(self, body_params): - return self.execute('PUT', 'authn/v1/saml/config', body_params=body_params) - - - """ - Enterprise Settings - - [ ] LIST - [ ] CREATE - [X] READ - [X] UPDATE - [ ] DELETE - """ - - def enterprise_settings_config(self, body_params): - return self.execute('POST', 'settings/enterprise', body_params=body_params) - - def enterprise_settings(self): - return self.execute('GET', 'settings/enterprise') - - """ - Anomaly Settings - - [ ] LIST - [ ] CREATE - [ ] READ - [X] UPDATE - [ ] DELETE - """ - - def anomaly_settings_config(self, body_params, policy_id): - anomaly_url = 'anomalies/settings/%s' % policy_id - return self.execute('POST', anomaly_url, body_params=body_params) - - """ - Check the other side - - [ ] LIST - [ ] CREATE - [X] READ - [X] UPDATE - [ ] DELETE - """ - - def check(self): - return self.execute('GET', 'check') diff --git a/prismacloud/api/cspm/cspm.py b/prismacloud/api/cspm/cspm.py deleted file mode 100644 index d25434b..0000000 --- a/prismacloud/api/cspm/cspm.py +++ /dev/null @@ -1,163 +0,0 @@ -""" Requests and Output """ - -import json -import time - -import requests - -class PrismaCloudAPIMixin(): - """ Requests and Output """ - - def suppress_warnings_when_verify_false(self): - if self.verify is False: - # Pylint Issue #4584 - # pylint: disable=no-member - requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) - - def login(self, url=None): - self.suppress_warnings_when_verify_false() - if not url: - url = 'https://%s/login' % self.api - action = 'POST' - request_headers = {'Content-Type': 'application/json'} - # Add User-Agent to the headers - request_headers['User-Agent'] = self.user_agent - body_params_json = json.dumps({'username': self.identity, 'password': self.secret}) - # try: - # api_response = requests.request(action, url, headers=request_headers, data=body_params_json, verify=self.verify, timeout=self.timeout) - # except requests.exceptions.Timeout: - # # continue in a retry loop - # except requests.exceptions.RequestException as ex: - # self.error_and_exit(api_response.status_code, 'API (%s) raised an exception\n%s' % (url, ex)) - api_response = requests.request(action, url, headers=request_headers, data=body_params_json, verify=self.verify, timeout=self.timeout) - if api_response.status_code in self.retry_status_codes: - for exponential_wait in self.retry_waits: - time.sleep(exponential_wait) - api_response = requests.request(action, url, headers=request_headers, data=body_params_json, verify=self.verify, timeout=self.timeout) - if api_response.ok: - break # retry loop - if api_response.ok: - api_response = json.loads(api_response.content) - self.token = api_response.get('token') - self.token_timer = time.time() - else: - self.error_and_exit(api_response.status_code, 'API (%s) responded with an error\n%s' % (url, api_response.text)) - if self.debug: - print('New API Token: %s' % self.token) - - def extend_login(self): - self.suppress_warnings_when_verify_false() - url = 'https://%s/auth_token/extend' % self.api - action = 'GET' - request_headers = {'Content-Type': 'application/json', 'x-redlock-auth': self.token} - # Add User-Agent to the headers - request_headers['User-Agent'] = self.user_agent - api_response = requests.request(action, url, headers=request_headers, verify=self.verify, timeout=self.timeout) - if api_response.status_code in self.retry_status_codes: - for exponential_wait in self.retry_waits: - time.sleep(exponential_wait) - api_response = requests.request(action, url, headers=request_headers, verify=self.verify, timeout=self.timeout) - if api_response.ok: - break # retry loop - if api_response.ok: - api_response = json.loads(api_response.content) - self.token = api_response.get('token') - self.token_timer = time.time() - else: - self.error_and_exit(api_response.status_code, 'API (%s) responded with an error\n%s' % (url, api_response.text)) - if self.debug: - print('Extending API Token') - - # pylint: disable=too-many-arguments, too-many-branches, too-many-locals - def execute(self, action, endpoint, query_params=None, body_params=None, request_headers=None, force=False, paginated=False): - self.suppress_warnings_when_verify_false() - if not self.token: - self.login() - if int(time.time() - self.token_timer) > self.token_limit: - self.extend_login() - # Endpoints that return large numbers of results use a 'nextPageToken' (and a 'totalRows') key. - # Pagination appears to be specific to "List Alerts V2 - POST" and the limit has a maximum of 10000. - more = True - results = [] - while more is True: - if int(time.time() - self.token_timer) > self.token_limit: - self.extend_login() - url = 'https://%s/%s' % (self.api, endpoint) - if not request_headers: - request_headers = {'Content-Type': 'application/json'} - if self.token: - request_headers['x-redlock-auth'] = self.token - if body_params: - body_params_json = json.dumps(body_params) - else: - body_params_json = None - self.debug_print('API URL: %s' % url) - self.debug_print('API Request Headers: (%s)' % request_headers) - self.debug_print('API Query Params: %s' % query_params) - self.debug_print('API Body Params: %s' % body_params_json) - # Add User-Agent to the headers - request_headers['User-Agent'] = self.user_agent - api_response = requests.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) - self.debug_print('API Response Status Code: %s' % api_response.status_code) - self.debug_print('API Response Headers: (%s)' % api_response.headers) - if api_response.status_code in self.retry_status_codes: - for exponential_wait in self.retry_waits: - time.sleep(exponential_wait) - api_response = requests.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) - if api_response.ok: - break # retry loop - if api_response.ok: - if not api_response.content: - return None - if api_response.headers.get('Content-Type') == 'application/x-gzip': - return api_response.content - if api_response.headers.get('Content-Type') == 'text/csv': - return api_response.content.decode('utf-8') - try: - result = json.loads(api_response.content) - #if result is None: - # self.logger.error('JSON returned None, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - # if force: - # return results # or continue - # self.error_and_exit(api_response.status_code, 'JSON returned None, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - except ValueError: - self.logger.error('JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - if force: - return results # or continue - self.error_and_exit(api_response.status_code, 'JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - if paginated: - results.extend(result['items']) - if 'nextPageToken' in result and result['nextPageToken']: - if self.debug: - print('Retrieving Next Page of Results') - body_params = {'pageToken': result['nextPageToken']} - more = True - else: - more = False - else: - return result - else: - self.logger.error('API: (%s) responded with a status of: (%s), with query: (%s) and body params: (%s)' % (url, api_response.status_code, query_params, body_params)) - if force: - return results - self.error_and_exit(api_response.status_code, 'API: (%s) with query params: (%s) and body params: (%s) responded with an error and this response:\n%s' % (url, query_params, body_params, api_response.text)) - return results - - # Exit handler (Error). - - @classmethod - def error_and_exit(cls, error_code, error_message='', system_message=''): - raise SystemExit('\n\nStatus Code: %s\n%s\n%s\n' % (error_code, error_message, system_message)) - - # Output counted errors. - - def error_report(self): - if self.logger.error.counter > 0: - print('API responded with (%s) error(s): details logged to: (%s)' % (self.logger.error.counter, self.error_log)) - - # Optionally output progress. - - @classmethod - def progress(cls, txt=None): - if txt: - print(txt) diff --git a/prismacloud/api/cwpp/_cloud.py b/prismacloud/api/cwpp/_cloud.py deleted file mode 100644 index ab5d799..0000000 --- a/prismacloud/api/cwpp/_cloud.py +++ /dev/null @@ -1,26 +0,0 @@ -""" Prisma Compute API Cloud Endpoints Class """ - -# Cloud - -class CloudPrismaCloudAPICWPPMixin: - """ Prisma Cloud Compute API Cloud Endpoints Class """ - - def cloud_discovery_read(self): - return self.execute_compute('GET', 'api/v1/cloud/discovery') - - def cloud_discovery_download(self, query_params=None): - # request_headers = {'Content-Type': 'text/csv'} - # return self.execute_compute('GET', 'api/v1/cloud/discovery/download?', request_headers=request_headers, query_params=query_params) - return self.execute_compute('GET', 'api/v1/cloud/discovery/download', query_params=query_params) - - def cloud_discovery_scan(self): - return self.execute_compute('POST', 'api/v1/cloud/discovery/scan') - - def cloud_discovery_scan_stop(self): - return self.execute_compute('POST', 'api/v1/cloud/discovery/stop') - - def cloud_discovery_vms(self, query_params=None): - return self.execute_compute('GET', 'api/v1/cloud/discovery/vms', query_params=query_params, paginated=True) - - def cloud_discovery_entities(self, query_params=None): - return self.execute_compute('GET', 'api/v1/cloud/discovery/entities', query_params=query_params) diff --git a/prismacloud/api/cwpp/_containers.py b/prismacloud/api/cwpp/_containers.py deleted file mode 100644 index 1346707..0000000 --- a/prismacloud/api/cwpp/_containers.py +++ /dev/null @@ -1,13 +0,0 @@ -""" Prisma Cloud Compute API Containers Endpoints Class """ - -# Containers - -class ContainersPrismaCloudAPICWPPMixin: - """ Prisma Cloud Compute API Containers Endpoints Class """ - - def containers_list_read(self, image_id=None, query_params=None): - if image_id: - containers = self.execute_compute('GET', 'api/v1/containers?imageId=%s' % image_id, query_params=query_params, paginated=True) - else: - containers = self.execute_compute('GET', 'api/v1/containers?', query_params=query_params, paginated=True) - return containers diff --git a/prismacloud/api/cwpp/_scans.py b/prismacloud/api/cwpp/_scans.py deleted file mode 100644 index c2687ec..0000000 --- a/prismacloud/api/cwpp/_scans.py +++ /dev/null @@ -1,13 +0,0 @@ -""" Prisma Cloud Compute API Scans Endpoints Class """ - -# Scans (Monitor > Vulnerabilities/Compliance > Images > CI) - -class ScansPrismaCloudAPICWPPMixin: - """ Prisma Cloud Compute API Scans Endpoints Class """ - - def scans_list_read(self, image_id=None): - if image_id: - images = self.execute_compute('GET', 'api/v1/scans?imageID=%s&filterBaseImage=true' % image_id) - else: - images = self.execute_compute('GET', 'api/v1/scans?filterBaseImage=true', paginated=True) - return images diff --git a/prismacloud/api/cwpp/cwpp.py b/prismacloud/api/cwpp/cwpp.py deleted file mode 100644 index cb0e750..0000000 --- a/prismacloud/api/cwpp/cwpp.py +++ /dev/null @@ -1,120 +0,0 @@ -""" Requests and Output """ - -import json -import time - -import requests - -class PrismaCloudAPICWPPMixin(): - """ Requests and Output """ - - def login_compute(self): - if self.api: - # Login via CSPM. - self.login() - elif self.api_compute: - # Login via CWP. - self.login('https://%s/api/v1/authenticate' % self.api_compute) - else: - self.error_and_exit(418, "Specify a Prisma Cloud URL or Prisma Cloud Compute URL") - self.debug_print('New API Token: %s' % self.token) - - def extend_login_compute(self): - # There is no extend for CWP, just logon again. - self.debug_print('Extending API Token') - self.login_compute() - - # pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements - def execute_compute(self, action, endpoint, query_params=None, body_params=None, request_headers=None, force=False, paginated=False): - self.suppress_warnings_when_verify_false() - if not self.token: - self.login_compute() - if not request_headers: - request_headers = {'Content-Type': 'application/json'} - if body_params: - body_params_json = json.dumps(body_params) - else: - body_params_json = None - # Set User Agent - request_headers['User-Agent'] = "W" - # Endpoints that return large numbers of results use a 'Total-Count' response header. - # Pagination is via query parameters for both GET and POST, and the limit has a maximum of 50. - offset = 0 - limit = 50 - more = False - results = [] - while offset == 0 or more is True: - if int(time.time() - self.token_timer) > self.token_limit: - self.extend_login_compute() - if paginated: - url = 'https://%s/%s?limit=%s&offset=%s' % (self.api_compute, endpoint, limit, offset) - else: - url = 'https://%s/%s' % (self.api_compute, endpoint) - if self.token: - if self.api: - # Authenticate via CSPM - request_headers['x-redlock-auth'] = self.token - else: - # Authenticate via CWP - request_headers['Authorization'] = "Bearer %s" % self.token - self.debug_print('API URL: %s' % url) - self.debug_print('API Request Headers: (%s)' % request_headers) - self.debug_print('API Query Params: %s' % query_params) - self.debug_print('API Body Params: %s' % body_params_json) - # Add User-Agent to the headers - request_headers['User-Agent'] = self.user_agent - api_response = requests.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) - self.debug_print('API Response Status Code: (%s)' % api_response.status_code) - self.debug_print('API Response Headers: (%s)' % api_response.headers) - if api_response.status_code in self.retry_status_codes: - for exponential_wait in self.retry_waits: - time.sleep(exponential_wait) - api_response = requests.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) - if api_response.ok: - break # retry loop - if api_response.ok: - if not api_response.content: - return None - if api_response.headers.get('Content-Type') == 'application/x-gzip': - return api_response.content - if api_response.headers.get('Content-Type') == 'text/csv': - return api_response.content.decode('utf-8') - try: - result = json.loads(api_response.content) - #if result is None: - # self.logger.error('JSON returned None, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - # if force: - # return results # or continue - # self.error_and_exit(api_response.status_code, 'JSON returned None, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - except ValueError: - self.logger.error('JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - if force: - return results # or continue - self.error_and_exit(api_response.status_code, 'JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - if 'Total-Count' in api_response.headers: - self.debug_print('Retrieving Next Page of Results: Offset/Total Count: %s/%s' % (offset, api_response.headers['Total-Count'])) - total_count = int(api_response.headers['Total-Count']) - if total_count > 0: - results.extend(result) - offset += limit - more = bool(offset < total_count) - else: - return result - else: - self.logger.error('API: (%s) responded with a status of: (%s), with query: (%s) and body params: (%s)' % (url, api_response.status_code, query_params, body_params)) - if force: - return results - self.error_and_exit(api_response.status_code, 'API: (%s) with query params: (%s) and body params: (%s) responded with an error and this response:\n%s' % (url, query_params, body_params, api_response.text)) - return results - - # The Compute API setting is optional. - - def validate_api_compute(self): - if not self.api_compute: - self.error_and_exit(500, 'Please specify a Prisma Cloud Compute API URL.') - - # Exit handler (Error). - - @classmethod - def error_and_exit(cls, error_code, error_message='', system_message=''): - raise SystemExit('\n\nStatus Code: %s\n%s\n%s\n' % (error_code, error_message, system_message)) diff --git a/prismacloud/api/pccs/pccs.py b/prismacloud/api/pccs/pccs.py deleted file mode 100644 index 3d1dfc6..0000000 --- a/prismacloud/api/pccs/pccs.py +++ /dev/null @@ -1,94 +0,0 @@ -""" Requests and Output """ - -import json -import time - -import requests - -class PrismaCloudAPIPCCSMixin(): - """ Requests and Output """ - - # pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements - def execute_code_security(self, action, endpoint, query_params=None, body_params=None, request_headers=None, force=False, paginated=False): - self.suppress_warnings_when_verify_false() - if not self.token: - self.login() - if int(time.time() - self.token_timer) > self.token_limit: - self.extend_login() - if not request_headers: - request_headers = {'Content-Type': 'application/json'} - if body_params: - body_params_json = json.dumps(body_params) - else: - body_params_json = None - # Endpoints that return large numbers of results use a 'hasNext' key. - # Pagination is via query parameters for both GET and POST, and appears to be specific to "List File Errors - POST". - offset = 0 - limit = 50 - more = False - results = [] - while offset == 0 or more is True: - if int(time.time() - self.token_timer) > self.token_limit: - self.extend_login() - if paginated: - url = 'https://%s/%s?limit=%s&offset=%s' % (self.api, endpoint, limit, offset) - else: - url = 'https://%s/%s' % (self.api, endpoint) - if self.token: - request_headers['authorization'] = self.token - self.debug_print('API URL: %s' % url) - self.debug_print('API Headers: %s' % request_headers) - self.debug_print('API Query Params: %s' % query_params) - self.debug_print('API Body Params: %s' % body_params_json) - # Add User-Agent to the headers - request_headers['User-Agent'] = self.user_agent - api_response = requests.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) - self.debug_print('API Response Status Code: %s' % api_response.status_code) - self.debug_print('API Response Headers: (%s)' % api_response.headers) - if api_response.status_code in self.retry_status_codes: - for exponential_wait in self.retry_waits: - time.sleep(exponential_wait) - api_response = requests.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) - if api_response.ok: - break # retry loop - if api_response.ok: - if not api_response.content: - return None - if api_response.headers.get('Content-Type') == 'application/x-gzip': - return api_response.content - if api_response.headers.get('Content-Type') == 'text/csv': - return api_response.content.decode('utf-8') - try: - result = json.loads(api_response.content) - #if result is None: - # self.logger.error('JSON returned None, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - # if force: - # return results # or continue - # self.error_and_exit(api_response.status_code, 'JSON returned None, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - except ValueError: - self.logger.error('JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - if force: - return results # or continue - self.error_and_exit(api_response.status_code, 'JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) - if paginated: - results.extend(result['data']) - if 'hasNext' in result: - self.debug_print('Retrieving Next Page of Results') - offset += limit - more = result['hasNext'] - else: - return results - else: - return result - else: - self.logger.error('API: (%s) responded with a status of: (%s), with query: (%s) and body params: (%s)' % (url, api_response.status_code, query_params, body_params)) - if force: - return results - self.error_and_exit(api_response.status_code, 'API: (%s) with query params: (%s) and body params: (%s) responded with an error and this response:\n%s' % (url, query_params, body_params, api_response.text)) - return results - - # Exit handler (Error). - - @classmethod - def error_and_exit(cls, error_code, error_message='', system_message=''): - raise SystemExit('\n\nStatus Code: %s\n%s\n%s\n' % (error_code, error_message, system_message)) diff --git a/prismacloud/api/version.py b/prismacloud/api/version.py deleted file mode 100644 index 369881d..0000000 --- a/prismacloud/api/version.py +++ /dev/null @@ -1 +0,0 @@ -version = "5.2.24" diff --git a/prismacloud/api/__init__.py b/prismacloudapi/__init__.py similarity index 54% rename from prismacloud/api/__init__.py rename to prismacloudapi/__init__.py index 019495b..6592acb 100644 --- a/prismacloud/api/__init__.py +++ b/prismacloudapi/__init__.py @@ -2,12 +2,8 @@ import sys -from .pc_lib_api import PrismaCloudAPI -from .pc_lib_utility import PrismaCloudUtility -from .version import version as api_version - -__author__ = 'Palo Alto Networks CSE/SE/SA Teams' -__version__ = api_version +from prismacloudapi.pc_lib_api import PrismaCloudAPI +from prismacloudapi.pc_lib_utility import PrismaCloudUtility MIN_PYTHON = (3, 6) if sys.version_info < MIN_PYTHON: diff --git a/prismacloud/api/cspm/README.md b/prismacloudapi/cspm/README.md similarity index 100% rename from prismacloud/api/cspm/README.md rename to prismacloudapi/cspm/README.md diff --git a/prismacloud/api/cspm/__init__.py b/prismacloudapi/cspm/__init__.py similarity index 100% rename from prismacloud/api/cspm/__init__.py rename to prismacloudapi/cspm/__init__.py diff --git a/prismacloudapi/cspm/_endpoints.py b/prismacloudapi/cspm/_endpoints.py new file mode 100644 index 0000000..2bd1db6 --- /dev/null +++ b/prismacloudapi/cspm/_endpoints.py @@ -0,0 +1,1254 @@ +""" Prisma Cloud API Endpoints Class """ +import logging +import pprint + +# TODO: Split into multiple files, one per endpoint ... + +# pylint: disable=too-many-public-methods + + +class EndpointsPrismaCloudAPIMixin(): + """ Prisma Cloud API Endpoints Class """ + + def current_user(self): + return self.execute('GET', 'user/me') + + """ + Note: Eventually, all objects covered should have full CRUD capability, ie, to create, read, update, and delete (and list). + + Template + + [ ] LIST + [ ] CREATE + [ ] READ + [ ] UPDATE + [ ] DELETE + """ + + """ + Alerts + + [x] LIST + [ ] CREATE + [ ] READ + [ ] UPDATE + [ ] DELETE + Additional: + [x] LIST (v2) + """ + + def alert_filter_suggest(self, query_params): + return self.execute('GET', 'filter/alert/suggest', query_params=query_params) + + def alert_filter_suggest_post(self, body_params): + return self.execute('POST', 'filter/alert/suggest', body_params=body_params) + + def alert_list_read(self, query_params=None, body_params=None): + # returns items directly + return self.execute('POST', 'alert', query_params=query_params, body_params=body_params) + + def alert_v2_list_read(self, query_params=None, body_params=None): + # returns items in results['items']. But really does not respect pagination request. + return self.execute_paginated('POST', 'v2/alert', query_params=query_params, body_params=body_params) + + def alert_info(self, alert_id, detailed=False): + return self.execute('GET', f'alert/{alert_id}', query_params={'detailed': detailed}) + + def alert_csv_create(self, body_params=None): + return self.execute('POST', 'alert/csv', body_params=body_params) + + def alert_csv_status(self, csv_report_id): + return self.execute('GET', 'alert/csv/%s/status' % csv_report_id) + + def alert_csv_download(self, csv_report_id): + return self.execute('GET', 'alert/csv/%s/download' % csv_report_id) + + def alert_count_by_policy(self, query_params): + """ + `https://pan.dev/prisma-cloud/api/cspm/get-alerts-grouped/` + """ + return self.execute('GET', 'alert/policy', query_params=query_params) + + def alert_count_by_policy_post(self, detailed, body_params): + """ + `https://pan.dev/prisma-cloud/api/cspm/post-alerts-grouped/` + """ + body_params['detailed'] = detailed + return self.execute('POST', 'alert/policy', body_params=body_params) + + def alert_count_by_status(self, status): + return self.execute('GET', f'alert/count/{status}') + + """ + Policies + + [x] LIST + [x] CREATE + [x] READ + [x] UPDATE + [x] DELETE + Additional: + [x] LIST (v2) + [x] LIST (v2) CUSTOM + [x] UPDATE STATUS + """ + + def policy_list_read(self): + return self.execute('GET', 'policy') + + def policy_v2_list_read(self): + return self.execute('GET', 'v2/policy') + + def policy_custom_v2_list_read(self): + filters = [('policy.policyMode', 'custom')] + return self.execute('GET', 'v2/policy', query_params=filters) + + def policy_create(self, policy_to_add): + return self.execute('POST', 'policy', body_params=policy_to_add) + + def policy_read(self, policy_id, message=None): + self.progress(message) + return self.execute('GET', 'policy/%s' % policy_id) + + def policy_update(self, policy_id, policy_update): + return self.execute('PUT', 'policy/%s' % policy_id, body_params=policy_update) + + def policy_status_update(self, policy_id, policy_status_update): + return self.execute('PATCH', 'policy/%s/status/%s' % (policy_id, policy_status_update)) + + def policy_delete(self, policy_id): + return self.execute('DELETE', 'policy/%s' % policy_id) + + """ + Saved Searches + + [x] LIST + [x] CREATE + [x] READ + [ ] UPDATE + [x] DELETE + """ + + def saved_search_list_read(self): + return self.execute('GET', 'search/history?filter=saved') + + def saved_search_create(self, type_of_search, saved_search_to_add): + if type_of_search == 'network': + return self.execute('POST', 'search', body_params=saved_search_to_add) + if type_of_search == 'audit_event': + return self.execute('POST', 'search/event', body_params=saved_search_to_add) + return self.execute('POST', 'search/%s' % type_of_search, body_params=saved_search_to_add) + + def saved_search_read(self, saved_search_id, message=None): + self.progress(message) + return self.execute('GET', 'search/history/%s' % saved_search_id) + + def saved_search_delete(self, saved_search_id): + return self.execute('DELETE', 'search/history/%s' % saved_search_id) + + """ + Compliance Posture + + [x] LIST + [ ] CREATE + [ ] READ + [ ] UPDATE + [ ] DELETE + """ + + def compliance_posture_statistics(self, query_params): + """Get Compliance Statistics Breakdown V2 + `PAN Api docs `_ + """ + return self.execute('GET', 'v2/compliance/posture', query_params=query_params) + + def compliance_posture_statistics_post(self, body_params): + """Get Compliance Statistics Breakdown V2""" + return self.execute('POST', 'v2/compliance/posture', body_params=body_params) + + def compliance_posture_statistics_for_standard(self, compliance_id, query_params): + """Get Compliance Statistics for Standard ID V2 + `PAN Api docs `_ + """ + return self.execute('GET', f'v2/compliance/posture/{compliance_id}', query_params=query_params) + + def compliance_posture_statistics_for_standard_post(self, compliance_id, body_params): + """Get Compliance Statistics for Standard ID V2""" + return self.execute('POST', f'v2/compliance/posture/{compliance_id}', body_params=body_params) + + def compliance_posture_statistics_for_requirement(self, compliance_id, requirement_id, query_params): + """Get Compliance Statistics for Requirement ID V2 + `PAN Api docs `_ + """ + return self.execute('GET', f'v2/compliance/posture/{compliance_id}/{requirement_id}', query_params=query_params) + + def compliance_posture_statistics_for_requirement_post(self, compliance_id, requirement_id, body_params): + """Get Compliance Statistics for Requirement ID V2""" + return self.execute('POST', f'v2/compliance/posture/{compliance_id}/{requirement_id}', body_params=body_params) + + def compliance_posture_trend(self, query_params): + """Get Compliance Trend V2 + `PAN Api docs `_ + """ + return self.execute('GET', 'v2/compliance/posture/trend', query_params=query_params) + + def compliance_posture_trend_post(self, body_params): + """Get Compliance Trend V2""" + return self.execute('POST', 'v2/compliance/posture/trend', body_params=body_params) + + def compliance_posture_trend_for_standard(self, compliance_id): + """Get Compliance Trend for Standard ID V2 + `PAN Api docs `_ + """ + return self.execute('GET', f'v2/compliance/posture/trend/{compliance_id}') + + def compliance_posture_trend_for_standard_post(self, compliance_id, body_params): + """Get Compliance Trend for Standard ID V2""" + return self.execute('POST', f'v2/compliance/posture/trend/{compliance_id}', body_params=body_params) + + def compliance_posture_trend_for_requirement(self, compliance_id, requirement_id, query_params): + """Get Compliance Trend for Requirement ID V2 + `PAN Api docs `_ + """ + return self.execute('GET', f'v2/compliance/posture/trend/{compliance_id}/{requirement_id}', query_params=query_params) + + def compliance_posture_trend_for_requirement_post(self, compliance_id, requirement_id, body_params): + """Get Compliance Trend for Requirement ID V2 """ + return self.execute('POST', f'v2/compliance/posture/trend/{compliance_id}/{requirement_id}', + body_params=body_params) + + """ + Compliance Standards + + [x] LIST + [x] CREATE + [x] READ + [ ] UPDATE + [x] DELETE + """ + + def compliance_standard_list_read(self): + return self.execute('GET', 'compliance') + + def compliance_standard_create(self, compliance_standard_to_add): + return self.execute('POST', 'compliance', body_params=compliance_standard_to_add) + + def compliance_standard_read(self, compliance_standard_id): + return self.execute('GET', 'compliance/%s' % compliance_standard_id) + + def compliance_standard_delete(self, compliance_standard_id): + return self.execute('DELETE', 'compliance/%s' % compliance_standard_id) + + """ + Compliance Standard Requirements + + [x] LIST + [x] CREATE + [ ] READ + [ ] UPDATE + [ ] DELETE + """ + + def compliance_standard_requirement_list_read(self, compliance_standard_id): + return self.execute('GET', 'compliance/%s/requirement' % compliance_standard_id) + + def compliance_standard_requirement_create(self, compliance_standard_id, compliance_requirement_to_add): + return self.execute('POST', 'compliance/%s/requirement' % compliance_standard_id, + body_params=compliance_requirement_to_add) + + """ + Compliance Standard Requirements Sections + + [x] LIST + [x] CREATE + [ ] READ + [ ] UPDATE + [ ] DELETE + """ + + def compliance_standard_requirement_section_list_read(self, compliance_requirement_id): + return self.execute('GET', 'compliance/%s/section' % compliance_requirement_id) + + def compliance_standard_requirement_section_create(self, compliance_requirement_id, compliance_section_to_add): + return self.execute('POST', 'compliance/%s/section' % compliance_requirement_id, + body_params=compliance_section_to_add) + + """ + Compliance Standard Requirements Policies + + [x] LIST + [ ] CREATE + [ ] READ + [ ] UPDATE + [ ] DELETE + Additional: + [x] LIST (v2) + + """ + + def compliance_standard_policy_list_read(self, compliance_standard_name): + filters = [('policy.complianceStandard', compliance_standard_name)] + return self.execute('GET', 'policy', query_params=filters) + + def compliance_standard_policy_v2_list_read(self, compliance_standard_name): + filters = [('policy.complianceStandard', compliance_standard_name)] + return self.execute('GET', 'v2/policy', query_params=filters) + + """ + Users + + [x] LIST + [x] CREATE + [x] READ + [x] UPDATE + [x] DELETE + """ + + def user_list_read_v3(self): + return self.execute('GET', 'v3/user') + + def user_list_read_v2(self): + return self.execute('GET', 'v2/user') + + def user_create(self, user): + return self.execute('POST', 'v2/user', body_params=user) + + def user_read(self, user_id): + return self.execute('GET', 'v2/user/%s' % user_id) + + def user_update(self, user): + return self.execute('PUT', 'v2/user/%s' % user['email'], body_params=user) + + def user_delete(self, user_id): + return self.execute('DELETE', 'user/%s' % user_id) + + def user_list_user_emails(self): + return self.execute('GET', 'user/name') + + def user_list_email_domains(self): + return self.execute('GET', 'user/domain') + + def user_list_bypass_sso(self): + return self.execute('GET', 'user/saml/bypass') + + def user_update_bypass_sso(self, body_params): + return self.execute('PUT', 'user/saml/bypass', body_params=body_params) + + """ + User Roles + + [x] LIST + [ ] CREATE + [x] READ + [x] UPDATE + [x] DELETE + """ + + def user_role_list_read(self): + return self.execute('GET', 'user/role') + + def user_role_create(self, user_role_to_add): + return self.execute('POST', 'user/role', body_params=user_role_to_add) + + def user_role_update(self, user_role_id, user_role_update): + return self.execute('PUT', 'user/role/%s' % user_role_id, body_params=user_role_update) + + def user_role_read(self, user_role_id): + return self.execute('GET', 'user/role/%s' % user_role_id) + + def user_role_delete(self, user_role_id): + return self.execute('DELETE', 'user/role/%s' % user_role_id) + + """ + Access Keys + + [x] LIST + [x] CREATE + [x] READ + [x] UPDATE + [x] DELETE + Additional: + [x] UPDATE STATUS + """ + + def access_keys_list_read(self): + return self.execute('GET', 'access_keys') + + def access_key_create(self, access_key_to_add): + return self.execute('POST', 'access_keys', body_params=access_key_to_add) + + def access_key_read(self, access_key_id): + return self.execute('GET', 'access_keys/%s' % access_key_id) + + def access_key_update(self, access_key_id, access_key_update): + return self.execute('PUT', 'access_keys/%s' % access_key_id, body_params=access_key_update) + + # Note: Expired keys cannot be enabled. + def access_key_status_update(self, access_key_id, access_key_status): + return self.execute('PATCH', 'access_keys/%s/status/%s' % (access_key_id, access_key_status)) + + def access_key_delete(self, access_key_id): + return self.execute('DELETE', 'access_keys/%s' % access_key_id) + + """ + Cloud Accounts + + [x] LIST + [x] CREATE + [ ] READ + [x] UPDATE + [x] DELETE + Additional: + [x] LIST NAMES + """ + + def cloud_accounts_list_read(self, query_params=None): + return self.execute('GET', 'cloud', query_params=query_params) + + def cloud_accounts_children_list_read(self, cloud_account_type, cloud_account_id): + return self.execute('GET', 'cloud/%s/%s/project' % (cloud_account_type, cloud_account_id)) + + def cloud_accounts_list_names_read(self, query_params=None): + return self.execute('GET', 'cloud/name', query_params=query_params) + + def cloud_accounts_create(self, cloud_type, cloud_account_to_add): + return self.execute('POST', 'cloud/%s' % cloud_type, body_params=cloud_account_to_add) + + def cloud_account_info_read(self, cloud_type, cloud_account_id): + return self.execute('GET', 'cloud/%s/%s' % (cloud_type, cloud_account_id)) + + def cloud_account_update(self, cloud_type, cloud_account_id, cloud_account_update): + return self.execute('PUT', 'cloud/%s/%s' % (cloud_type, cloud_account_id), body_params=cloud_account_update) + + def cloud_account_delete(self, cloud_type, cloud_account_id): + return self.execute('DELETE', 'cloud/%s/%s' % (cloud_type, cloud_account_id)) + + def cloud_account_status(self, cloud_account_id): + return self.execute('GET', f'account/{cloud_account_id}/config/status') + + def cloud_types_list_read(self, query_params=None): + return self.execute('GET', 'cloud/type', query_params=query_params) + + """ + Cloud Account Groups + + [x] LIST + [x] CREATE + [x] READ + [x] UPDATE + [x] DELETE + """ + + def cloud_account_group_list_read(self,exclude_details: bool=None, include_pending_accounts:bool=None): + """ + `PAN Api docs `_ + """ + query_params = {} + if exclude_details is not None: + query_params['excludeAccountGroupDetails'] = exclude_details + if include_pending_accounts is not None: + query_params['includePendingAccounts'] = include_pending_accounts + return self.execute('GET', 'cloud/group', query_params=query_params) + + def cloud_account_group_create(self, name, description, account_id_list: list): + """ + Create an account group. + + `PAN Api docs `_ + """ + body_params=dict(name=name, description=description, accountIds=account_id_list) + return self.execute('POST', 'cloud/group', body_params=body_params) + + def cloud_account_group_read(self, cloud_account_group_id): + return self.execute('GET', 'cloud/group/%s' % cloud_account_group_id) + + def cloud_account_group_update(self, cloud_account_group_id, name=None, description=None, account_id_list=None): + body_params=dict(id=cloud_account_group_id) + if name is not None: + body_params['name'] = name + if description is not None: + body_params['description'] = description + if account_id_list is not None: + body_params['accountIds'] = account_id_list + return self.execute('PUT', 'cloud/group/%s' % cloud_account_group_id, body_params=body_params) + + def cloud_account_group_delete(self, cloud_account_group_id): + return self.execute('DELETE', 'cloud/group/%s' % cloud_account_group_id) + + """ + Asset (Resources) Inventory + + [x] LIST + [ ] CREATE + [ ] READ + [ ] UPDATE + [ ] DELETE + Additional: + [x] LIST (v2) + [x] LIST WITH FILTERS(v2) + """ + + def asset_inventory_list_read(self, query_params=None): + return self.execute('GET', 'v2/inventory', query_params=query_params) + + def asset_inventory_list_read_post(self, body_params=None): + # timeRange just doesn't work here + return self.execute('POST', 'v2/inventory', body_params=body_params) + + """ + Asset (Resources) Inventory V3 + + [x] LIST + [ ] CREATE + [ ] READ + [ ] UPDATE + [ ] DELETE + Additional: + [x] LIST (v3) + [x] LIST WITH FILTERS(v3) + """ + + def asset_inventory_list_read_v3(self, query_params=None): + return self.execute('GET', 'v3/inventory', query_params=query_params) + + def asset_inventory_list_read_postv_3(self, body_params=None): + return self.execute('POST', 'v3/inventory', body_params=body_params) + + def asset_inventory_trend_list_read_v3(self, query_params=None): + return self.execute('GET', 'v3/inventory/trend', query_params=query_params) + + def asset_inventory_trend_list_read_postv_3(self, body_params=None): + return self.execute('POST', 'v3/inventory/trend', body_params=body_params) + + def asset_inventory_v4(self, body_params=None): + # timeRange just doesn't work here + return self.execute('POST', 'api/v4/inventory', body_params=body_params) + + def asset_inventory_count(self, body_params=None): + return self.execute('POST', 'api/v4/aggregation/count', body_params=body_params) + + def asset_inventory_filters(self, query_params=None): + return self.execute('GET', 'filter/v2/inventory/suggest', query_params=query_params) + + def asset_inventory_filters_post(self, body_params=None): + return self.execute('POST', 'filter/v2/inventory/suggest', body_params=body_params) + + + """ + (Assets) Resources + + [ ] LIST + [ ] CREATE + [x] READ + [ ] UPDATE + [ ] DELETE + """ + + def resource_read(self, body_params=None, force=False, message=None): + self.progress(message) + return self.execute('POST', 'resource', body_params=body_params, force=force) + + def resource_network_read(self, body_params=None, force=False): + return self.execute('POST', 'resource/network', body_params=body_params, force=force) + + def resource_scan_info_read(self, body_params=None): + result = [] + page_number = 1 + while page_number == 1 or 'pageToken' in body_params: + api_response = self.execute( + 'POST', 'resource/scan_info', body_params=body_params) + if 'resources' in api_response: + result.extend(api_response['resources']) + if 'nextPageToken' in api_response: + body_params['pageToken'] = api_response['nextPageToken'] + else: + body_params.pop('pageToken', None) + # if 'totalMatchedCount' in api_response: + # self.progress('Resources: %s, Page Size: %s, Page: %s' % (api_response['totalMatchedCount'], body_params['limit'], page_number)) + page_number += 1 + return result + + def resource_scan_info_read_v2(self, body_params=None): + page_number = 1 + while page_number == 1 or 'pageToken' in body_params: + api_response = self.execute( + 'POST', 'api/v2/resource/scan_info', body_params=body_params) + if 'resources' in api_response: + yield from api_response['resources'] + if 'nextPageToken' in api_response: + body_params['pageToken'] = api_response['nextPageToken'] + else: + body_params.pop('pageToken', None) + # if 'totalMatchedCount' in api_response: + # self.progress('Resources: %s, Page Size: %s, Page: %s' % (api_response['totalMatchedCount'], body_params['limit'], page_number)) + page_number += 1 + return + + def resource_scan_info_read_v4(self, body_params=None): + page_number = 1 + while page_number == 1 or 'nextPageToken' in body_params: + api_response = self.execute( + 'POST', 'api/v4/resource/scan_info', body_params=body_params) + if 'resources' in api_response: + yield from api_response['resources'] + if 'nextPageToken' in api_response: + body_params['nextPageToken'] = api_response['nextPageToken'] + else: + body_params.pop('nextPageToken', None) + # if 'totalMatchedCount' in api_response: + # self.progress('Resources: %s, Page Size: %s, Page: %s' % (api_response['totalMatchedCount'], body_params['limit'], page_number)) + page_number += 1 + return + + + """ + Alert Rules + + [x] LIST + [x] CREATE + [x] READ + [x] UPDATE + [x] DELETE + Additional: + [x] LIST (v2) + """ + + def alert_rule_list_read(self): + return self.execute('GET', 'v2/alert/rule') + + def alert_rule_create(self, alert_rule): + return self.execute('POST', 'alert/rule', body_params=alert_rule) + + def alert_rule_read(self, alert_rule_id): + return self.execute('GET', 'alert/rule/%s' % alert_rule_id) + + def alert_rule_delete(self, alert_rule_id): + return self.execute('DELETE', 'alert/rule/%s' % alert_rule_id) + + def alert_rule_update(self, alert_rule_id, alert_rule_update): + return self.execute('PUT', 'alert/rule/%s' % alert_rule_id, body_params=alert_rule_update) + + """ + Integrations + + [x] LIST + [ ] CREATE + [ ] READ + [ ] UPDATE + [x] DELETE + Additional: + [ ] LIST (v2) + """ + + def integration_list_read(self): + return self.execute('GET', 'integration') + + def integration_delete(self, integration_id): + return self.execute('DELETE', 'integration/%s' % integration_id) + + def integration_list(self, tenant_id): + # use + return self.execute('GET', f'v1/tenant/{tenant_id}/integration') + + """ + Resource Lists + + [x] LIST + [X] CREATE + [ ] READ + [ ] UPDATE + [x] DELETE + """ + + def resource_list_read(self): + return self.execute('GET', 'v1/resource_list') + + def resource_list_types_read(self): + return self.execute('GET', 'v1/resource_list/types') + + def resource_list_delete(self, resource_list_id): + return self.execute('DELETE', 'v1/resource_list/%s' % resource_list_id) + + def resource_list_create(self, resource_list_to_add): + return self.execute('POST', 'v1/resource_list', body_params=resource_list_to_add) + + """ + Adoption Advisor + + [x] LIST + [X] CREATE + [ ] READ + [X] UPDATE + [X] DELETE + """ + + def adoptionadvisor_report_read(self): + return self.execute('GET', 'adoptionadvisor/report') + + def adoptionadvisor_report_delete(self, report_id): + return self.execute('DELETE', 'adoptionadvisor/report/%s' % report_id) + + def adoptionadvisor_report_create(self, report_to_add): + return self.execute('POST', 'adoptionadvisor/report', body_params=report_to_add) + + """ + Compliance Reports + + [x] LIST + [x] CREATE + [ ] READ + [ ] UPDATE + [x] DELETE + Additional: + [x] DOWNLOAD + """ + + def compliance_report_list_read(self): + return self.execute('GET', 'report') + + def compliance_report_create(self, report_to_add): + return self.execute('POST', 'report', body_params=report_to_add) + + def compliance_report_create_v2(self, name, cloud_type, locale=None, target=None, type=None): + body_params = dict(name=name, cloudType=cloud_type) + if locale: + body_params['locale'] = locale + if target: + body_params['target'] = target + if type: + body_params['type'] = type + return self.execute('POST', 'v2/report', body_params=body_params) + + def compliance_report_get_config(self, report_id): + return self.execute('GET', f'report/{report_id}') + + def compliance_report_delete(self, report_id): + return self.execute('DELETE', f'report/{report_id}') + + def compliance_report_download(self, report_id): + """ + Download Report + `PAN Api docs `_ + """ + return self.execute('GET', f'report/{report_id}/download') + + def compliance_report_history(self, report_id): + return self.execute('GET', f'report/{report_id}/history') + + def compliance_report_type_list(self): + """ + Get Report Types + `PAN Api docs `_ + """ + return self.execute('GET', 'report/type') + + def compliance_report_type_get(self, report_id): + """ + Get Report Config + `PAN Api docs `_ + """ + return self.execute('GET', f'report/type/{report_id}') + + def compliance_report_filter_suggest(self): + """ + Get Report filter suggest + `PAN Api docs `_ + """ + return self.execute('GET', f'filter/report/suggest') + + def jobs_report_metadata(self, report_types=None): + """ + This endpoint is available on the Prisma Cloud Darwin release only. + + Get Reports Metadata + `PAN Api docs `_ + """ + query_params = {} + if report_types: + query_params['report_types'] = report_types + return self.execute('GET', 'report-service/api/v1/report', query_params=query_params) + + def jobs_report_metadata_by_id(self, report_id): + """ + This endpoint is available on the Prisma Cloud Darwin release only. + + Background jobs - Get Reports - by report id + `PAN Api docs `_ + """ + return self.execute('GET', f'report-service/api/v1/report/{report_id}') + + def jobs_report_status(self, report_id): + """ + This endpoint is available on the Prisma Cloud Darwin release only. + + Background jobs - Get Reports - get status + `PAN Api docs `_ + """ + return self.execute('GET', f'report-service/api/v1/report/{report_id}/status') + + def jobs_report_download(self, report_id): + """ + This endpoint is available on the Prisma Cloud Darwin release only. + + Background jobs - Get Reports - download + `PAN Api docs `_ + """ + return self.execute('GET', f'report-service/api/v1/report/{report_id}/download') + + """ + Search + + [ ] LIST + [ ] CREATE + [x] READ + [ ] UPDATE + [ ] DELETE + """ + + def search_config_read(self, query, search_id=None, search_name=None, search_description=None, limit=100, + with_resource_json=None, time_range=None, sort=None, heuristic_search=None, + paginate=True): + """ + Perform Config Search + `PAN Api docs `_ + """ + # if time_range is None: + # time_range = dict(type="relative", value=dict(unit="hour", amount=24)) + body_params = dict(query=query, id=search_id, searchName=search_name, searchDescription=search_description, + limit=limit, withResourceJson=with_resource_json, timeRange=time_range, sort=sort, + heuristicSearch=heuristic_search) + for k, v in dict(body_params).items(): + if v is None: + del body_params[k] + next_page_token = None + # https://pan.dev/prisma-cloud/api/cspm/search-config/ + api_response = self.execute('POST', 'search/config', body_params=body_params) + # logging.debug(pprint.pformat(api_response)) + if 'data' in api_response and 'items' in api_response['data']: + yield from api_response['data']['items'] + next_page_token = api_response['data'].pop('nextPageToken', None) + while paginate and next_page_token: + body_params['pageToken'] = next_page_token + # https://pan.dev/prisma-cloud/api/cspm/search-config-page/ + # logging.debug("paging %s", pprint.pformat(body_params)) + api_response = self.execute('POST', 'search/config/page', body_params=body_params) + # logging.debug(pprint.pformat(api_response)) + if 'items' in api_response: + yield from api_response['items'] + next_page_token = api_response.pop('nextPageToken', None) + return + + def search_config_read_by_query(self, query, skip_search_creation=None, skip_results=None, limit=100, + with_resource_json=None, time_range=None, + sort=None, next_page_token=None, paginate=True): + """ + Perform Config Search by Query + `PAN Api docs `_ + """ + # if time_range is None: + # time_range = dict(type="relative", value=dict(unit="hour", amount=24)) + body_params = dict(query=query, skipSearchCreation=skip_search_creation, limit=limit, + withResourceJson=with_resource_json, timeRange=time_range, + skipResult=skip_results, + sort=sort, + nextPageToken=next_page_token) + for k, v in dict(body_params).items(): + if v is None: + del body_params[k] + next_page_token = None + api_response = self.execute('POST', f'search/api/v1/config', body_params=body_params) + if 'items' in api_response: + yield from api_response['items'] + next_page_token = api_response.pop('nextPageToken', None) + while paginate and next_page_token: + body_params['nextPageToken'] = next_page_token + api_response = self.execute('POST', f'search/api/v1/config', body_params=body_params) + if 'items' in api_response: + yield from api_response['items'] + next_page_token = api_response.pop('nextPageToken', None) + return + + def search_config_read_by_search_id(self, search_id, limit=100, with_resource_json=None, time_range=None, + sort=None, next_page_token=None, paginate=True): + """ + Perform Config Search by Search Id + `PAN Api docs `_ + """ + # if time_range is None: + # time_range = dict(type="relative", value=dict(unit="hour", amount=24)) + body_params = dict(limit=limit, withResourceJson=with_resource_json, timeRange=time_range, sort=sort, + nextPageToken=next_page_token) + for k, v in dict(body_params).items(): + if v is None: + del body_params[k] + next_page_token = None + api_response = self.execute('POST', f'search/api/v1/config/{search_id}', body_params=body_params) + if 'items' in api_response: + yield from api_response['items'] + next_page_token = api_response.pop('nextPageToken', None) + while paginate and next_page_token: + body_params['nextPageToken'] = next_page_token + api_response = self.execute('POST', f'search/api/v1/config/{search_id}', body_params=body_params) + if 'items' in api_response: + yield from api_response['items'] + next_page_token = api_response.pop('nextPageToken', None) + return + + def search_config_read_v2(self, query, start_time=None, skip_results=None, limit=100, + with_resource_json=None, # time_range=None, + sort=None, next_page_token=None, paginate=True): + """ + Perform Config Search V2 + `PAN Api docs `_ + """ + # if time_range is None: + # time_range = dict(type="relative", value=dict(unit="hour", amount=24)) + body_params = dict(query=query, limit=limit, startTime=start_time, + withResourceJson=with_resource_json, # timeRange=time_range, + skipResult=skip_results, + sort=sort, + nextPageToken=next_page_token) + for k, v in dict(body_params).items(): + if v is None: + del body_params[k] + next_page_token = None + api_response = self.execute('POST', f'search/api/v2/config', body_params=body_params) + if 'items' in api_response: + yield from api_response['items'] + next_page_token = api_response.pop('nextPageToken', None) + while paginate and next_page_token: + body_params['nextPageToken'] = next_page_token + api_response = self.execute('POST', f'search/api/v2/config', body_params=body_params) + if 'items' in api_response: + yield from api_response['items'] + next_page_token = api_response.pop('nextPageToken', None) + return + + def search_network_read(self, search_params, filtered=False): + search_url = 'search' + if filtered: + search_url = 'search/filtered' + return self.execute('POST', search_url, body_params=search_params) + + def search_event_read(self, search_params, subsearch=None): + result = [] + next_page_token = None + search_url = 'search/event' + if subsearch and subsearch in ['aggregate', 'filtered']: + search_url = 'search/event/%s' % subsearch + api_response = self.execute_paginated( + 'POST', search_url, body_params=search_params) + if 'data' in api_response and 'items' in api_response['data']: + result = api_response['data']['items'] + next_page_token = api_response['data'].pop('nextPageToken', None) + while next_page_token: + api_response = self.execute_paginated( + 'POST', 'search/config/page', body_params={'limit': 1000, 'pageToken': next_page_token}) + if 'items' in api_response: + result.extend(api_response['items']) + next_page_token = api_response.pop('nextPageToken', None) + return result + + def search_iam_read(self, search_params): + result = [] + next_page_token = None + api_response = self.execute_paginated( + 'POST', 'api/v1/permission', body_params=search_params) + if 'data' in api_response and 'items' in api_response['data']: + result = api_response['data']['items'] + next_page_token = api_response['data'].pop('nextPageToken', None) + while next_page_token: + api_response = self.execute_paginated( + 'POST', 'api/v1/permission/page', + body_params={'limit': 1000, 'pageToken': next_page_token, 'withResourceJson': 'true'}) + if 'items' in api_response: + result.extend(api_response['items']) + next_page_token = api_response.pop('nextPageToken', None) + return result + + def search_iam_source_to_granter(self, search_params): + search_url = 'api/v1/permission/graph/source_to_granter' + return self.execute('POST', search_url, body_params=search_params) + + def search_iam_granter_to_dest(self, search_params): + search_url = 'api/v1/permission/graph/granter_to_dest' + return self.execute('POST', search_url, body_params=search_params) + + def search_suggest_list_read(self, query_to_suggest): + return self.execute('POST', 'search/suggest', body_params=query_to_suggest) + + def get_permissions_v4(self, query, limit=100, search_id=None, groupByFields=None, paginate=True): + """ + Returns permissions grouped by requested fields and a page token for the next page if applicable. + + `PAN Api docs `_ + """ + body_params = dict(query=query) + if search_id: + body_params['search_id'] = search_id + if groupByFields: + body_params['groupByFields'] = groupByFields + # + next_page_token = None + api_response = self.execute('POST', 'iam/api/v4/search/permission', query_params=dict(limit=limit), body_params=body_params) + if 'data' in api_response and 'items' in api_response['data']: + yield from api_response['data']['items'] + next_page_token = api_response['data'].pop('nextPageToken', None) + while paginate and next_page_token: + body_params['nextPageToken'] = next_page_token + api_response = self.execute('POST', 'iam/api/v4/search/permission', query_params=dict(limit=limit), body_params=body_params) + if 'items' in api_response['data']: + yield from api_response['data']['items'] + next_page_token = api_response.pop('nextPageToken', None) + + + """ + Configuration + + [ ] LIST + [ ] CREATE + [x] READ + [ ] UPDATE + [ ] DELETE + """ + + def compute_config(self): + return self.execute('GET', 'compute/config') + + def meta_info(self): + return self.execute('GET', 'meta_info') + + """ + Usage + + [ ] LIST + [ ] CREATE + [x] READ + [ ] UPDATE + [ ] DELETE + """ + + def resource_usage_by_cloud_type(self, body_params): + return self.execute('POST', 'license/api/v1/usage', body_params=body_params) + + def resource_usage_over_time(self, body_params): + return self.execute('POST', 'license/api/v1/usage/time_series', body_params=body_params) + + def resource_usage_by_cloud_type_v2(self, body_params): + return self.execute('POST', 'license/api/v2/usage', body_params=body_params) + + def resource_usage_over_time_v2(self, body_params): + return self.execute('POST', 'license/api/v2/time_series', body_params=body_params) + + """ + SSO SAML + + [X] LIST + [X] CREATE + [X] READ + [X] UPDATE + [ ] DELETE + """ + + def saml_config_read(self): + return self.execute('GET', 'authn/v1/saml/config') + + def saml_config_create(self, body_params): + return self.execute('POST', 'authn/v1/saml/config', body_params=body_params) + + def saml_config_update(self, body_params): + return self.execute('PUT', 'authn/v1/saml/config', body_params=body_params) + + def oidc_config_read(self): + """ + Get OIDC Configuration + `PAN Api docs `_ + """ + return self.execute('GET', 'authn/v1/oauth2/config') + + """ + Permission groups + """ + + def permission_group_list(self): + """ + Get All Permission Groups + `PAN Api docs `_ + """ + return self.execute('GET', 'authz/v1/permission_group') + + def permission_group_get(self, group_id, include_associated_roles: bool = None): + """ + Get Permission Group by ID + `PAN Api docs `_ + """ + query_params = dict() + if include_associated_roles: + query_params = dict(includeAssociatedRoles=include_associated_roles) + return self.execute('GET', f'authz/v1/permission_group/{group_id}', query_params=query_params) + + def permission_group_feature_list(self): + """ + Get All Active Features + `PAN Api docs `_ + """ + return self.execute('GET', 'authz/v1/feature') + + """ + Enterprise Settings + + [ ] LIST + [ ] CREATE + [X] READ + [X] UPDATE + [ ] DELETE + """ + + def enterprise_settings_config(self, body_params): + return self.execute('POST', 'settings/enterprise', body_params=body_params) + + def enterprise_settings(self): + return self.execute('GET', 'settings/enterprise') + + """ + Anomaly Settings + + [ ] LIST + [ ] CREATE + [ ] READ + [X] UPDATE + [ ] DELETE + """ + + def anomaly_settings_config(self, body_params, policy_id): + anomaly_url = 'anomalies/settings/%s' % policy_id + return self.execute('POST', anomaly_url, body_params=body_params) + + """ + Check the other side + + [ ] LIST + [ ] CREATE + [X] READ + [X] UPDATE + [ ] DELETE + """ + + def check(self): + return self.execute('GET', 'check') + + """ + Notifications Templates + """ + + def templates_list(self): + """ + List Templates + `PAN Api docs `_ + """ + return self.execute('GET', f'api/v1/tenant/{self.tenant_id}/template') + + def templates_get(self, template_id): + """ + Get Template + `PAN Api docs `_ + """ + return self.execute('GET', f'api/v1/tenant/{self.tenant_id}/template/{template_id}') + + """ + Cloud Ingested Logs + """ + + def aws_eventbridge_configuration_for_account(self, tenant_id, account_id): + """ + Get AWS Eventbridge configuration details + `PAN Api docs `_ + """ + return self.execute('GET', f'audit_logs/v2/tenant/{tenant_id}/aws_accounts/{account_id}/eventbridge_config') + + def aws_eventbridge_configuration_for_account(self, account_id): + """ + Fetch AWS S3 Flow Log details + `PAN Api docs `_ + """ + return self.execute('GET', + f'cloud-accounts-manager/v1/cloud-accounts/aws/{account_id}/features/aws-flow-logs/s3') + + """ + CSPM collections + """ + + def cspm_collections_list_read(self): + """ + Get all collections. + + Note this is different from CWP Collections + `PAN Api docs `_ + """ + query_params = dict() + # can't use paginated, data in data['value'] instead of 'items'. nextPageToken if more than X + result = self.execute('GET', f'entitlement/api/v1/collection', query_params=query_params) + while True: + yield from result['value'] + if result['nextPageToken']: + query_params = dict(nextPageToken=result['nextPageToken']) + result = self.execute('GET', f'entitlement/api/v1/collection', query_params=query_params) + else: + break + return + + def cspm_collections_get(self, collection_id): + """ + Get collection by id. + + `PAN Api docs `_ + """ + return self.execute('GET', f'entitlement/api/v1/collection/{collection_id}') + + def cspm_collections_create(self, name, description, account_id_list: list = None, + account_group_id_list: list = None, repository_id_list: list = None): + """ + Create a Collection. + + `PAN Api docs `_ + """ + body_params=dict(name=name, description=description) + if account_id_list or account_group_id_list or repository_id_list: + body_params['assetGroups'] = dict() + if account_id_list: + body_params['assetGroups']['accountIds'] = account_id_list + if account_group_id_list: + body_params['assetGroups']['accountGroupIds'] = account_group_id_list + if repository_id_list: + body_params['assetGroups']['repositoryIds'] = repository_id_list + return self.execute('POST', 'entitlement/api/v1/collection', body_params=body_params) + + def cspm_collections_update(self, collection_id, name=None, description=None, account_id_list: list = None, + account_group_id_list: list = None, repository_id_list: list = None): + """ + Update a Collection. + + `PAN Api docs `_ + """ + body_params=dict() + if name: + body_params['name'] = name + if description: + body_params['description'] = description + if account_id_list or account_group_id_list or repository_id_list: + body_params['assetGroups'] = dict() + if account_id_list: + body_params['assetGroups']['accountIds'] = account_id_list + if account_group_id_list: + body_params['assetGroups']['accountGroupIds'] = account_group_id_list + if repository_id_list: + body_params['assetGroups']['repositoryIds'] = repository_id_list + return self.execute('PUT', f'entitlement/api/v1/collection/{collection_id}', body_params=body_params) + + def cspm_collections_delete(self, collection_id): + """ + Delete a Collection. + + `PAN Api docs `_ + """ + return self.execute('DELETE', f'entitlement/api/v1/collection/{collection_id}') \ No newline at end of file diff --git a/prismacloud/api/cspm/_extended.py b/prismacloudapi/cspm/_extended.py similarity index 100% rename from prismacloud/api/cspm/_extended.py rename to prismacloudapi/cspm/_extended.py diff --git a/prismacloudapi/cspm/cspm.py b/prismacloudapi/cspm/cspm.py new file mode 100644 index 0000000..fcf8c47 --- /dev/null +++ b/prismacloudapi/cspm/cspm.py @@ -0,0 +1,172 @@ +""" Requests and Output """ + +import json +import logging +import time + +import requests + +class PrismaCloudAPIMixin(): + """ Requests and Output """ + + def suppress_warnings_when_verify_false(self): + if self.verify is False: + # Pylint Issue #4584 + # pylint: disable=no-member + requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + + def login(self, url=None): + self.suppress_warnings_when_verify_false() + if not url: + # CSPM + url = f'https://{self.api}/login' + # remove previous tokens + if 'x-redlock-auth' in self.session.headers: + del self.session.headers['x-redlock-auth'] + body_params_json = json.dumps({'username': self.identity, 'password': self.secret}) + api_response = self.session.post(url, data=body_params_json, verify=self.verify, timeout=self.timeout) + if api_response.ok: + api_response = api_response.json() + self.token = api_response.get('token') + self.token_timer = time.time() + self.session.headers['x-redlock-auth'] = self.token + # save tenant_id + self.tenant_id = api_response['customerNames'][0]['prismaId'] + else: + self.error_and_raise(api_response.status_code, 'API (%s) responded with an error\n%s' % (url, api_response.text)) + self.debug_print('New API Token: %s' % self.token) + + def extend_login(self): + self.suppress_warnings_when_verify_false() + self.debug_print('Extending CSPM API Token') + url = f'https://{self.api}/auth_token/extend' + api_response = self.session.get(url, verify=self.verify, timeout=self.timeout) + if api_response.ok: + api_response = api_response.json() + self.token = api_response.get('token') + self.token_timer = time.time() + self.session.headers['x-redlock-auth'] = self.token + else: + logging.warning(f'HTTP error code {api_response.status_code} - API ({url}) responded with an error - lets try to login again\n {api_response.text}') + # try to login again + self.login() + + # pylint: disable=too-many-arguments, too-many-branches, too-many-locals + def execute(self, action, endpoint, query_params=None, body_params=None, request_headers=None, force=False): + self.suppress_warnings_when_verify_false() + if not self.token: + self.login() + if int(time.time() - self.token_timer) > self.token_limit: + self.extend_login() + # Endpoints that return large numbers of results use a 'nextPageToken' (and a 'totalRows') key. + # Pagination appears to be specific to "List Alerts V2 - POST" and the limit has a maximum of 10000. + url = f'https://{self.api}/{endpoint}' + if body_params: + body_params_json = json.dumps(body_params) + else: + body_params_json = None + self.debug_print('API URL: %s' % url) + self.debug_print('API Request Headers: (%s)' % request_headers) + self.debug_print('API Query Params: %s' % query_params) + self.debug_print('API Body Params: %s' % body_params_json) + api_response = self.session.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) + self.debug_print('API Response Status Code: %s' % api_response.status_code) + self.debug_print('API Response Headers: (%s)' % api_response.headers) + if api_response.ok: + if not api_response.content: + return None + if api_response.headers.get('Content-Type') == 'application/x-gzip': + return api_response.content + if api_response.headers.get('Content-Type') == 'text/csv': + return api_response.content.decode('utf-8') + try: + result = api_response.json() + except ValueError: + self.logger.error('JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + if force: + return None + self.error_and_raise(api_response.status_code, 'JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + return result + else: + self.logger.error('API: (%s) responded with a status of: (%s), with query: (%s) and body params: (%s)' % (url, api_response.status_code, query_params, body_params)) + self.error_and_raise(api_response.status_code, 'API: (%s) with query params: (%s) and body params: (%s) responded with an error and this response:\n%s' % (url, query_params, body_params, api_response.text)) + return None + + def execute_paginated(self, action, endpoint, query_params=None, body_params=None, request_headers=None): + self.suppress_warnings_when_verify_false() + if not self.token: + self.login() + if int(time.time() - self.token_timer) > self.token_limit: + self.extend_login() + # Endpoints that return large numbers of results use a 'nextPageToken' (and a 'totalRows') key. + # Pagination appears to be specific to "List Alerts V2 - POST" and the limit has a maximum of 10000. + returned_count = 0 + more = True + while more is True: + if int(time.time() - self.token_timer) > self.token_limit: + self.extend_login() + url = f'https://{self.api}/{endpoint}' + if body_params: + body_params_json = json.dumps(body_params) + else: + body_params_json = None + self.debug_print('API URL: %s' % url) + self.debug_print('API Request Headers: (%s)' % request_headers) + self.debug_print('API Query Params: %s' % query_params) + self.debug_print('API Body Params: %s' % body_params_json) + api_response = self.session.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) + self.debug_print('API Response Status Code: %s' % api_response.status_code) + self.debug_print('API Response Headers: (%s)' % api_response.headers) + if api_response.ok: + if not api_response.content: + return + if api_response.headers.get('Content-Type') == 'application/x-gzip': + # return api_response.content + raise RuntimeError("please use .execute instead of execute_paginated") + if api_response.headers.get('Content-Type') == 'text/csv': + #return api_response.content.decode('utf-8') + raise RuntimeError("please use .execute instead of execute_paginated") + try: + result = api_response.json() + except ValueError: + self.logger.error('JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + self.error_and_raise(api_response.status_code, 'JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + return + if 'totalRows' in result: + total_count = int(result['totalRows']) + self.debug_print(f'Retrieved Next Page of Results: Offset/Total Count: {returned_count}/{total_count}') + returned_count += len(result['items']) + # + yield from result['items'] + if 'nextPageToken' in result and result['nextPageToken']: + self.debug_print('Retrieving Next Page of Results') + body_params = {'pageToken': result['nextPageToken']} + more = True + else: + more = False + else: + self.logger.error('API: (%s) responded with a status of: (%s), with query: (%s) and body params: (%s)' % (url, api_response.status_code, query_params, body_params)) + self.error_and_raise(api_response.status_code, 'API: (%s) with query params: (%s) and body params: (%s) responded with an error and this response:\n%s' % (url, query_params, body_params, api_response.text)) + return + # Exit handler (Error). + + @classmethod + def error_and_exit(cls, error_code, error_message='', system_message=''): + raise SystemExit('\n\nStatus Code: %s\n%s\n%s\n' % (error_code, error_message, system_message)) + + @classmethod + def error_and_raise(cls, error_code, error_message='', system_message=''): + raise RuntimeError('\n\nStatus Code: %s\n%s\n%s\n' % (error_code, error_message, system_message)) + + # Output counted errors. + + def error_report(self): + if self.logger.error.counter > 0: + print('API responded with (%s) error(s): details logged to: (%s)' % (self.logger.error.counter, self.error_log)) + + # Optionally output progress. + + @classmethod + def progress(cls, txt=None): + if txt: + print(txt) diff --git a/prismacloud/api/cwpp/README.md b/prismacloudapi/cwpp/README.md similarity index 100% rename from prismacloud/api/cwpp/README.md rename to prismacloudapi/cwpp/README.md diff --git a/prismacloud/api/cwpp/__init__.py b/prismacloudapi/cwpp/__init__.py similarity index 100% rename from prismacloud/api/cwpp/__init__.py rename to prismacloudapi/cwpp/__init__.py diff --git a/prismacloud/api/cwpp/_audits.py b/prismacloudapi/cwpp/_audits.py similarity index 86% rename from prismacloud/api/cwpp/_audits.py rename to prismacloudapi/cwpp/_audits.py index 49af2ab..c7f2ed3 100644 --- a/prismacloud/api/cwpp/_audits.py +++ b/prismacloudapi/cwpp/_audits.py @@ -8,7 +8,7 @@ class AuditsPrismaCloudAPICWPPMixin: # Reference: https://prisma.pan.dev/api/cloud/cwpp/audits def audits_list_read(self, audit_type='incidents', query_params=None): - audits = self.execute_compute('GET', 'api/v1/audits/%s' % audit_type, query_params=query_params, paginated=True) + audits = self.execute_compute_paginated('GET', 'api/v1/audits/%s' % audit_type, query_params=query_params) return audits # Other related and undocumented endpoints. @@ -22,7 +22,7 @@ def forensic_read(self, workload_id, workload_type, defender_hostname): elif workload_type == 'host': response = self.execute_compute('GET', 'api/v1/profiles/%s/%s/forensic/download' % (workload_type, workload_id), query_params=query_params) else: - response = self.execute_compute('GET', 'api/v1/profiles/%s/%s/forensic' % (workload_type, workload_id), query_params=query_params, paginated=True) + response = self.execute_compute_paginated('GET', 'api/v1/profiles/%s/%s/forensic' % (workload_type, workload_id), query_params=query_params) return response # Monitor / Runtime > Incident Explorer @@ -63,7 +63,7 @@ def compute_audit_types(): # Hosts > Host Activities def host_forensic_activities_list_read(self, query_params=None): - audits = self.execute_compute('GET', 'api/v1/forensic/activities', query_params=query_params, paginated=True) + audits = self.execute_compute_paginated('GET', 'api/v1/forensic/activities', query_params=query_params) return audits # Compute > Manage > History diff --git a/prismacloudapi/cwpp/_cloud.py b/prismacloudapi/cwpp/_cloud.py new file mode 100644 index 0000000..48dd3ab --- /dev/null +++ b/prismacloudapi/cwpp/_cloud.py @@ -0,0 +1,64 @@ +""" Prisma Compute API Cloud Endpoints Class """ + + +# Cloud + +class CloudPrismaCloudAPICWPPMixin: + """ Prisma Cloud Compute API Cloud Endpoints Class """ + + def cloud_discovery_read(self, sort=None, reverse=None, + provider=None, credential_id=None, service_type=None, registry=None, account_name=None, + agentless=None, zone=None, + ): + """ + Returns a list of all cloud discovery scan results in a paginated response. + `PAN Api docs `_ + """ + query_params = dict(sort=sort, reverse=reverse, + provider=provider, credentialID=credential_id, serviceType=service_type, registry=registry, + accountName=account_name, agentless=agentless, + zone=zone, + ) + for k, v in dict(query_params).items(): + if v is None: + del query_params[k] + elif isinstance(v, list): + query_params[k] = ','.join(v) + return self.execute_compute_paginated('GET', 'api/v1/cloud/discovery', query_params=query_params) + + def cloud_discovery_download(self, query_params=None): + # request_headers = {'Content-Type': 'text/csv'} + # return self.execute_compute('GET', 'api/v1/cloud/discovery/download?', request_headers=request_headers, query_params=query_params) + return self.execute_compute('GET', 'api/v1/cloud/discovery/download', query_params=query_params) + + def cloud_discovery_scan(self): + return self.execute_compute('POST', 'api/v1/cloud/discovery/scan') + + def cloud_discovery_scan_stop(self): + return self.execute_compute('POST', 'api/v1/cloud/discovery/stop') + + def cloud_discovery_vms(self, query_params=None): + return self.execute_compute_paginated('GET', 'api/v1/cloud/discovery/vms', query_params=query_params) + + def cloud_discovery_entities(self, query_params=None): + return self.execute_compute_paginated('GET', 'api/v1/cloud/discovery/entities', query_params=query_params) + + def cloud_discovery_entities2(self, sort=None, reverse=None, credential_id=None, service_type=None, registry=None, + zone=None, defended=None, images=None, + ): + """ + Returns a list of discovered cloud entities. + `PAN Api docs `_ + """ + query_params = dict( + sort=sort, reverse=reverse, + credentialID=credential_id, serviceType=service_type, registry=registry, + zone=zone, + defended=defended, images=images + ) + for k, v in dict(query_params).items(): + if v is None: + del query_params[k] + elif isinstance(v, list): + query_params[k] = ','.join(v) + return self.execute_compute_paginated('GET', 'api/v1/cloud/discovery/entities', query_params=query_params) diff --git a/prismacloud/api/cwpp/_collections.py b/prismacloudapi/cwpp/_collections.py similarity index 81% rename from prismacloud/api/cwpp/_collections.py rename to prismacloudapi/cwpp/_collections.py index 8847c87..a262a57 100644 --- a/prismacloud/api/cwpp/_collections.py +++ b/prismacloudapi/cwpp/_collections.py @@ -5,11 +5,12 @@ class CollectionsPrismaCloudAPICWPPMixin: """ Prisma Cloud Compute API Collections Endpoints Class """ - def collections_list_read(self, query_params=None): - return self.execute_compute('GET', 'api/v1/collections', query_params=query_params, paginated=True) + def collections_list_read(self, **kwargs): + query_params=kwargs + return self.execute_compute('GET', 'api/v1/collections', query_params=query_params) def collection_usages(self, collection_id): - return self.execute_compute('GET', 'api/v1/collections/%s/usages' % collection_id, paginated=True) + return self.execute_compute_paginated('GET', 'api/v1/collections/%s/usages' % collection_id) # Note: No response is returned upon successful execution of POST, PUT, and DELETE. # You must verify the collection via collections_list_read() or the Console. diff --git a/prismacloudapi/cwpp/_containers.py b/prismacloudapi/cwpp/_containers.py new file mode 100644 index 0000000..f46d1d0 --- /dev/null +++ b/prismacloudapi/cwpp/_containers.py @@ -0,0 +1,17 @@ +""" Prisma Cloud Compute API Containers Endpoints Class """ + +# Containers + +class ContainersPrismaCloudAPICWPPMixin: + """ Prisma Cloud Compute API Containers Endpoints Class """ + + def containers_list_read(self, image_id=None, query_params=None): + if image_id: + containers = self.execute_compute_paginated('GET', 'api/v1/containers?imageId=%s' % image_id, query_params=query_params) + else: + containers = self.execute_compute_paginated('GET', 'api/v1/containers?', query_params=query_params) + return containers + + def containers_download(self, query_params=None): + containers = self.execute_compute('GET', 'api/v1/containers/download?', query_params=query_params) + return containers diff --git a/prismacloud/api/cwpp/_credentials.py b/prismacloudapi/cwpp/_credentials.py similarity index 100% rename from prismacloud/api/cwpp/_credentials.py rename to prismacloudapi/cwpp/_credentials.py diff --git a/prismacloud/api/cwpp/_defenders.py b/prismacloudapi/cwpp/_defenders.py similarity index 54% rename from prismacloud/api/cwpp/_defenders.py rename to prismacloudapi/cwpp/_defenders.py index 5d5f47e..cb465c5 100644 --- a/prismacloud/api/cwpp/_defenders.py +++ b/prismacloudapi/cwpp/_defenders.py @@ -6,9 +6,13 @@ class DefendersPrismaCloudAPICWPPMixin: """ Prisma Cloud Compute API Defenders Endpoints Class """ def defenders_list_read(self, query_params=None): - defenders = self.execute_compute('GET', 'api/v1/defenders', query_params=query_params, paginated=True) + defenders = self.execute_compute_paginated('GET', 'api/v1/defenders', query_params=query_params) return defenders def defenders_names_list_read(self, query_params=None): - defenders = self.execute_compute('GET', 'api/v1/defenders/names', query_params=query_params, paginated=True) + defenders = self.execute_compute_paginated('GET', 'api/v1/defenders/names', query_params=query_params) + return defenders + + def defenders_download(self, query_params=None): + defenders = self.execute_compute('GET', 'api/v1/defenders/download?', query_params=query_params) return defenders diff --git a/prismacloud/api/cwpp/_feeds.py b/prismacloudapi/cwpp/_feeds.py similarity index 100% rename from prismacloud/api/cwpp/_feeds.py rename to prismacloudapi/cwpp/_feeds.py diff --git a/prismacloud/api/cwpp/_hosts.py b/prismacloudapi/cwpp/_hosts.py similarity index 76% rename from prismacloud/api/cwpp/_hosts.py rename to prismacloudapi/cwpp/_hosts.py index 6893cfe..39439ff 100644 --- a/prismacloud/api/cwpp/_hosts.py +++ b/prismacloudapi/cwpp/_hosts.py @@ -7,11 +7,11 @@ class HostsPrismaCloudAPICWPPMixin: # Running hosts table in Monitor > Vulnerabilities > Hosts > Running Hosts def hosts_list_read(self, query_params=None): - hosts = self.execute_compute('GET', 'api/v1/hosts', query_params=query_params, paginated=True) + hosts = self.execute_compute_paginated('GET', 'api/v1/hosts', query_params=query_params) return hosts def hosts_info_list_read(self, query_params=None): - hosts = self.execute_compute('GET', 'api/v1/hosts/info', query_params=query_params, paginated=True) + hosts = self.execute_compute_paginated('GET', 'api/v1/hosts/info', query_params=query_params) return hosts def hosts_download(self, query_params=None): diff --git a/prismacloud/api/cwpp/_images.py b/prismacloudapi/cwpp/_images.py similarity index 56% rename from prismacloud/api/cwpp/_images.py rename to prismacloudapi/cwpp/_images.py index d5a612c..8ea731c 100644 --- a/prismacloud/api/cwpp/_images.py +++ b/prismacloudapi/cwpp/_images.py @@ -5,11 +5,11 @@ class ImagesPrismaCloudAPICWPPMixin: """ Prisma Cloud Compute API Images Endpoints Class """ - def images_list_read(self, image_id=None, query_params=None): - if image_id: - images = self.execute_compute('GET', 'api/v1/images?id=%s' % image_id, query_params=query_params) - else: - images = self.execute_compute('GET', 'api/v1/images?', query_params=query_params, paginated=True) + def images_get_read(self, image_id, query_params=None): + return self.execute_compute('GET', 'api/v1/images?id=%s' % image_id, query_params=query_params) + + def images_list_read(self, query_params=None): + images = self.execute_compute_paginated('GET', 'api/v1/images?', query_params=query_params) return images def images_download(self, query_params=None): diff --git a/prismacloud/api/cwpp/_logs.py b/prismacloudapi/cwpp/_logs.py similarity index 100% rename from prismacloud/api/cwpp/_logs.py rename to prismacloudapi/cwpp/_logs.py diff --git a/prismacloud/api/cwpp/_policies.py b/prismacloudapi/cwpp/_policies.py similarity index 100% rename from prismacloud/api/cwpp/_policies.py rename to prismacloudapi/cwpp/_policies.py diff --git a/prismacloud/api/cwpp/_registry.py b/prismacloudapi/cwpp/_registry.py similarity index 62% rename from prismacloud/api/cwpp/_registry.py rename to prismacloudapi/cwpp/_registry.py index a9563aa..3ac8591 100644 --- a/prismacloud/api/cwpp/_registry.py +++ b/prismacloudapi/cwpp/_registry.py @@ -5,11 +5,15 @@ class RegistryPrismaCloudAPICWPPMixin: """ Prisma Cloud Compute API Images Endpoints Class """ - def registry_list_read(self, image_id=None): - if image_id: - images = self.execute_compute('GET', 'api/v1/registry?id=%s&filterBaseImage=true' % image_id) - else: - images = self.execute_compute('GET', 'api/v1/registry?filterBaseImage=true', paginated=True) + def registry_download(self, query_params=None): + registries = self.execute_compute('GET', 'api/v1/registry/download?', query_params=query_params) + return registries + + def registry_get_read(self, image_id): + return self.execute_compute('GET', 'api/v1/registry?id=%s&filterBaseImage=true' % image_id) + + def registry_list_read(self): + images = self.execute_compute_paginated('GET', 'api/v1/registry?filterBaseImage=true') return images def registry_list_image_names(self, query_params=None): diff --git a/prismacloudapi/cwpp/_scans.py b/prismacloudapi/cwpp/_scans.py new file mode 100644 index 0000000..b3a1d0c --- /dev/null +++ b/prismacloudapi/cwpp/_scans.py @@ -0,0 +1,17 @@ +""" Prisma Cloud Compute API Scans Endpoints Class """ + +# Scans (Monitor > Vulnerabilities/Compliance > Images > CI) + +class ScansPrismaCloudAPICWPPMixin: + """ Prisma Cloud Compute API Scans Endpoints Class """ + + def scans_get(self, image_id): + return self.execute_compute('GET', 'api/v1/scans?imageID=%s&filterBaseImage=true' % image_id) + + def scans_list_read(self): + images = self.execute_compute_paginated('GET', 'api/v1/scans?filterBaseImage=true') + return images + + def scans_download(self, query_params=None): + scans = self.execute_compute('GET', 'api/v1/scans/download?', query_params=query_params) + return scans diff --git a/prismacloud/api/cwpp/_serverless.py b/prismacloudapi/cwpp/_serverless.py similarity index 59% rename from prismacloud/api/cwpp/_serverless.py rename to prismacloudapi/cwpp/_serverless.py index eb4acb3..a1c60e1 100644 --- a/prismacloud/api/cwpp/_serverless.py +++ b/prismacloudapi/cwpp/_serverless.py @@ -3,14 +3,18 @@ class ServerlessPrismaCloudAPICWPPMixin: # Get serverless function scan results def serverless_list_read(self, query_params=None): - result = self.execute_compute('GET', 'api/v1/serverless', query_params=query_params, paginated=True) - return result - + return self.execute_compute_paginated('GET', 'api/v1/serverless', query_params=query_params) + # Download serverless function scan results def serverless_download(self, query_params=None): - result = self.execute_compute('GET', 'api/v1/serverless/download?', query_params=query_params) - return result - + return self.execute_compute('GET', 'api/v1/serverless/download', query_params=query_params) + + def serverless_get_function_names(self, query_params=None): + """ + Get Serverless Function Names + """ + return self.execute_compute_paginated('GET', 'api/v1/serverless/names', query_params=query_params) + # Start serverless function scan def serverless_start_scan(self): result = self.execute_compute('POST', 'api/v1/serverless/scan') diff --git a/prismacloud/api/cwpp/_settings.py b/prismacloudapi/cwpp/_settings.py similarity index 100% rename from prismacloud/api/cwpp/_settings.py rename to prismacloudapi/cwpp/_settings.py diff --git a/prismacloud/api/cwpp/_stats.py b/prismacloudapi/cwpp/_stats.py similarity index 82% rename from prismacloud/api/cwpp/_stats.py rename to prismacloudapi/cwpp/_stats.py index 5353aab..f44bcc2 100644 --- a/prismacloud/api/cwpp/_stats.py +++ b/prismacloudapi/cwpp/_stats.py @@ -22,7 +22,7 @@ def stats_compliance_refresh(self, query_params=None): def stats_daily_read(self): # Returns a historical list of per-day statistics for the resources protected by Prisma Cloud Compute, # including the total number of runtime audits, image vulnerabilities, and compliance violations. - return self.execute_compute('GET', 'api/v1/stats/daily', paginated=True) + return self.execute_compute('GET', 'api/v1/stats/daily') def stats_trends_read(self): # Returns statistics about the resources protected by Prisma Cloud Compute, @@ -38,19 +38,23 @@ def stats_license_read(self): def stats_vulnerabilities_read(self, query_params=None): # Returns a list of vulnerabilities (CVEs) in the deployed images, registry images, hosts, and serverless functions affecting your environment. - return self.execute_compute('GET', 'api/v1/stats/vulnerabilities?', query_params=query_params, paginated=True) + # 2025-03 doesn't really use pagination. + return self.execute_compute('GET', 'api/v1/stats/vulnerabilities', query_params=query_params) def stats_vulnerabilities_download(self, query_params=None): - return self.execute_compute('GET', 'api/v1/stats/vulnerabilities/download?', query_params=query_params) + return self.execute_compute('GET', 'api/v1/stats/vulnerabilities/download', query_params=query_params) - def stats_vulnerabilities_impacted_resoures_read(self, query_params=None): + def stats_vulnerabilities_impacted_resources_read(self, query_params=None): # Generates a list of impacted resources for a specific vulnerability. This endpoint returns a list of all deployed images, registry images, hosts, and serverless functions affected by a given CVE. - return self.execute_compute('GET', 'api/v1/stats/vulnerabilities/impacted-resources?', query_params=query_params) + return self.execute_compute('GET', 'api/v1/stats/vulnerabilities/impacted-resources', query_params=query_params) - def stats_vulnerabilities_impacted_resoures_download(self, query_params=None): - return self.execute_compute('GET', 'api/v1/stats/vulnerabilities/impacted-resources/download?', query_params=query_params) + def stats_vulnerabilities_impacted_resources_download(self, query_params=None): + return self.execute_compute('GET', 'api/v1/stats/vulnerabilities/impacted-resources/download', query_params=query_params) def stats_vulnerabilities_refresh(self, query_params=None): # Refreshes the current day's CVE counts and CVE list, as well as their descriptions. # This endpoint returns the same response as /api/v1/stats/vulnerabilities, but with updated data for the current day. - return self.execute_compute('GET', 'api/v1/stats/vulnerabilities/refresh?', query_params=query_params, paginated=True) + return self.execute_compute('GET', 'api/v1/stats/vulnerabilities/refresh', query_params=query_params) + + def stats_assets_summary(self): + return self.execute_compute('GET', 'api/v1/bff/assets/summary') diff --git a/prismacloud/api/cwpp/_status.py b/prismacloudapi/cwpp/_status.py similarity index 100% rename from prismacloud/api/cwpp/_status.py rename to prismacloudapi/cwpp/_status.py diff --git a/prismacloud/api/cwpp/_tags.py b/prismacloudapi/cwpp/_tags.py similarity index 100% rename from prismacloud/api/cwpp/_tags.py rename to prismacloudapi/cwpp/_tags.py diff --git a/prismacloud/api/cwpp/_vms.py b/prismacloudapi/cwpp/_vms.py similarity index 72% rename from prismacloud/api/cwpp/_vms.py rename to prismacloudapi/cwpp/_vms.py index e82d8d4..f642973 100644 --- a/prismacloud/api/cwpp/_vms.py +++ b/prismacloudapi/cwpp/_vms.py @@ -8,6 +8,5 @@ class VMsPrismaCloudAPICWPPMixin: # VM Image table in Monitor > Vulnerabilities > Hosts > VMs def vms_list_read(self, query_params=None): - vms = self.execute_compute( - 'GET', 'api/v1/vms', query_params=query_params, paginated=True) + vms = self.execute_compute_paginated('GET', 'api/v1/vms', query_params=query_params) return vms \ No newline at end of file diff --git a/prismacloudapi/cwpp/cwpp.py b/prismacloudapi/cwpp/cwpp.py new file mode 100644 index 0000000..23255a1 --- /dev/null +++ b/prismacloudapi/cwpp/cwpp.py @@ -0,0 +1,160 @@ +""" Requests and Output """ + +import json +import time + +import requests + +class PrismaCloudAPICWPPMixin(): + """ Requests and Output """ + + def login_compute(self): + self.suppress_warnings_when_verify_false() + # CWP + url = f'https://{self.api_compute}/api/v1/authenticate' + # remove previous tokens + if 'Authorization' in self.session_compute.headers: + del self.session_compute.headers['Authorization'] + body_params_json = json.dumps({'username': self.identity, 'password': self.secret}) + api_response = self.session_compute.post(url, data=body_params_json, verify=self.verify, timeout=self.timeout) + if api_response.ok: + api_response = api_response.json() + self.token_compute = api_response.get('token') + self.token_compute_timer = time.time() + self.session_compute.headers['Authorization'] = f"Bearer {self.token_compute}" + else: + self.error_and_raise(api_response.status_code, + 'API (%s) responded with an error\n%s' % (url, api_response.text)) + + def check_extend_login_compute(self): + # There is no extend for CWP, just logon again. + if not self.token_compute or (int(time.time() - self.token_compute_timer) > self.token_limit): + self.token_compute = None + self.debug_print('Extending CWPP API Token') + self.login_compute() + + # def _check_ + + # pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements + def execute_compute(self, action, endpoint, query_params=None, body_params=None): + self.suppress_warnings_when_verify_false() + self.check_extend_login_compute() + if body_params: + body_params_json = json.dumps(body_params) + else: + body_params_json = None + # Endpoints that return large numbers of results use a 'Total-Count' response header. + # Pagination is via query parameters for both GET and POST, and the limit has a maximum of 50. + url = 'https://%s/%s' % (self.api_compute, endpoint) + self.debug_print('API URL: %s' % url) + self.debug_print('API Request Headers: (%s)' % self.session_compute.headers) + self.debug_print('API Query Params: %s' % query_params) + self.debug_print('API Body Params: %s' % body_params_json) + api_response = self.session_compute.request(action, url, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) + self.debug_print('API Response Status Code: (%s)' % api_response.status_code) + self.debug_print('API Response Headers: (%s)' % api_response.headers) + # if api_response.status_code in self.retry_status_codes: + # for exponential_wait in self.retry_waits: + # time.sleep(exponential_wait) + # api_response = requests.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) + # if api_response.ok: + # break # retry loop + if api_response.ok: + if not api_response.content: + return None + if api_response.headers.get('Content-Type') == 'application/x-gzip': + return api_response.content + if api_response.headers.get('Content-Type') == 'text/csv': + return api_response.content.decode('utf-8') + try: + result = api_response.json() + except ValueError: + self.logger.error('JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + self.error_and_raise(api_response.status_code, 'JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + return result + else: + self.logger.error('API: (%s) responded with a status of: (%s), with query: (%s) and body params: (%s)' % (url, api_response.status_code, query_params, body_params)) + self.error_and_raise(api_response.status_code, 'API: (%s) with query params: (%s) and body params: (%s) responded with an error and this response:\n%s' % (url, query_params, body_params, api_response.text)) + return + + def execute_compute_paginated(self, action, endpoint, query_params=None, body_params=None): + self.suppress_warnings_when_verify_false() + self.check_extend_login_compute() + if body_params: + body_params_json = json.dumps(body_params) + else: + body_params_json = None + # Endpoints that return large numbers of results use a 'Total-Count' response header. + # Pagination is via query parameters for both GET and POST, and the limit has a maximum of 100. + offset = 0 + limit = 100 + more = True + while more is True: + self.check_extend_login_compute() + url = f'https://{self.api_compute}/{endpoint}?limit={limit}&offset={offset}' + self.debug_print('API URL: %s' % url) + self.debug_print('API Request Headers: (%s)' % self.session_compute.headers) + self.debug_print('API Query Params: %s' % query_params) + self.debug_print('API Body Params: %s' % body_params_json) + api_response = self.session_compute.request(action, url, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) + self.debug_print('API Response Status Code: (%s)' % api_response.status_code) + self.debug_print('API Response Headers: (%s)' % api_response.headers) + if api_response.ok: + if not api_response.content: + return + if api_response.headers.get('Content-Type') == 'application/x-gzip': + # return api_response.content + raise RuntimeError("please use .execute instead of execute_paginated") + if api_response.headers.get('Content-Type') == 'text/csv': + # return api_response.content.decode('utf-8') + raise RuntimeError("please use .execute instead of execute_paginated") + try: + results = api_response.json() + except ValueError: + self.logger.error('JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + self.error_and_raise(api_response.status_code, 'JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + if 'Total-Count' in api_response.headers: + total_count = int(api_response.headers['Total-Count']) + self.debug_print(f'Retrieving Next Page of Results: Offset/Total Count: {offset}/{total_count}') + else: + self.debug_print("No Pagination headers - please use .execute instead of execute_paginated") + if results: + yield from results + return + # raise RuntimeError("please use .execute instead of execute_paginated") + if not results: + return + self.debug_print(f"Got {len(results)} results") + if total_count > 0: + yield from results + offset += len(results) + more = bool(offset < total_count) + else: + self.logger.error('API: (%s) responded with a status of: (%s), with query: (%s) and body params: (%s)' % (url, api_response.status_code, query_params, body_params)) + self.error_and_raise(api_response.status_code, 'API: (%s) with query params: (%s) and body params: (%s) responded with an error and this response:\n%s' % (url, query_params, body_params, api_response.text)) + return + + # The Compute API setting is optional. + + def validate_api_compute(self): + if not self.api_compute: + self.error_and_exit(500, 'Please specify a Prisma Cloud Compute API URL.') + + # Exit handler (Error). + + @classmethod + def error_and_exit(cls, error_code, error_message='', system_message=''): + raise SystemExit('\n\nStatus Code: %s\n%s\n%s\n' % (error_code, error_message, system_message)) + + @classmethod + def error_and_raise(cls, error_code, error_message='', system_message=''): + raise RuntimeError('\n\nStatus Code: %s\n%s\n%s\n' % (error_code, error_message, system_message)) + + # various API + + def version(self): + return self.execute_compute('GET', 'api/v1/version') + + def ping(self): + # unauthenticated call + return self.session_compute.get(f'https://{self.api_compute}/api/v1/_ping').text diff --git a/prismacloud/api/pc_lib_api.py b/prismacloudapi/pc_lib_api.py similarity index 66% rename from prismacloud/api/pc_lib_api.py rename to prismacloudapi/pc_lib_api.py index 01289f5..ac0e614 100644 --- a/prismacloud/api/pc_lib_api.py +++ b/prismacloudapi/pc_lib_api.py @@ -2,12 +2,19 @@ import logging +import requests +from requests.adapters import HTTPAdapter, Retry + from .cspm import PrismaCloudAPICSPM from .cwpp import PrismaCloudAPICWPP from .pccs import PrismaCloudAPIPCCS from .pc_lib_utility import PrismaCloudUtility -from .version import version # Import version from your version.py + +import importlib.metadata +version = importlib.metadata.version("prismacloudapi") + + # --Description-- # @@ -38,18 +45,33 @@ def __init__(self): self.debug = False # self.timeout = None # timeout=(16, 300) + self.tenant_id = None self.token = None self.token_timer = 0 self.token_limit = 590 # aka 9 minutes + self.token_compute = None + self.token_compute_timer= 0 self.retry_status_codes = [425, 429, 500, 502, 503, 504] self.retry_waits = [1, 2, 4, 8, 16, 32] + self.retry_allowed_methods = frozenset(["GET", "POST"]) self.max_workers = 8 # self.error_log = 'error.log' self.logger = None # Set User-Agent - default_user_agent = f"PrismaCloudAPI/{version}" # Dynamically set default User-Agent - self.user_agent = default_user_agent + self.user_agent = f"PrismaCloudAPI/{version}" # Dynamically set default User-Agent + # use a session + self.session = requests.session() + self.session_compute = requests.session() + retries = Retry(total=6, status=6, backoff_factor=1, status_forcelist=self.retry_status_codes, + allowed_methods=self.retry_allowed_methods) + self.session_adapter = HTTPAdapter(max_retries=retries) + # CSPM + self.session.headers['User-Agent'] = self.user_agent + self.session.headers['Content-Type'] = 'application/json' + # CWP + self.session_compute.headers['User-Agent'] = self.user_agent + self.session_compute.headers['Content-Type'] = 'application/json' def __repr__(self): return 'Prisma Cloud API:\n API: (%s)\n Compute API: (%s)\n API Error Count: (%s)\n API Token: (%s)' % (self.api, self.api_compute, self.logger.error.counter, self.token) @@ -76,17 +98,25 @@ def configure(self, settings, use_meta_info=True): if url.endswith('.prismacloud.io') or url.endswith('.prismacloud.cn'): # URL is a Prisma Cloud CSPM API URL. self.api = url + self.session.mount(f"https://{url}", self.session_adapter) + self.debug_print(f"Mounted retry adapter on API {url}") # Use the Prisma Cloud CSPM API to identify the Prisma Cloud CWP API URL. if use_meta_info: meta_info = self.meta_info() if meta_info and 'twistlockUrl' in meta_info: self.api_compute = PrismaCloudUtility.normalize_url(meta_info['twistlockUrl']) + self.session.mount(f"https://{self.api_compute}", self.session_adapter) + self.debug_print(f"Mounted retry adapter on API Compute {self.api_compute}") else: # URL is a Prisma Cloud CWP API URL. self.api_compute = PrismaCloudUtility.normalize_url(url) + self.session.mount(f"https://{self.api_compute}", self.session_adapter) + self.debug_print(f"Mounted retry adapter on API Compute {self.api_compute}") + if not self.api and not self.api_compute: + self.error_and_exit(418, "Specify a Prisma Cloud URL or Prisma Cloud Compute URL") # Conditional printing. def debug_print(self, message): if self.debug: - print(message) + logging.debug(message) diff --git a/prismacloud/api/pc_lib_utility.py b/prismacloudapi/pc_lib_utility.py similarity index 96% rename from prismacloud/api/pc_lib_utility.py rename to prismacloudapi/pc_lib_utility.py index 24441ff..4b286d9 100644 --- a/prismacloud/api/pc_lib_utility.py +++ b/prismacloudapi/pc_lib_utility.py @@ -8,8 +8,6 @@ import os import sys -from update_checker import UpdateChecker -from .version import version as api_version try: # pylint: disable=redefined-builtin @@ -38,14 +36,6 @@ class PrismaCloudUtility(): CONFIG_DIRECTORY = os.path.join(homefolder, '.prismacloud') DEFAULT_CONFIG_FILE = os.path.join(CONFIG_DIRECTORY, 'credentials.json') - @classmethod - def package_version_check(cls, package_name='prismacloud-api'): - package_version_message = 'version: %s' % api_version - checker = UpdateChecker() - result = checker.check(package_name, api_version) - if result: - package_version_message = "version update available: %s -> %s\nrun 'pip3 install --upgrade %s' to update" % (api_version, result.available_version, package_name) - return package_version_message # Default command line arguments. # (Sync with pcs_configure.py.) @@ -110,7 +100,6 @@ def get_arg_parser(self): '--debug', action='store_true', help='(Optional) - Output debugging information') - get_arg_parser.epilog=self.package_version_check() return get_arg_parser # Read arguments from the command line and/or a settings file. diff --git a/prismacloud/api/pccs/README.md b/prismacloudapi/pccs/README.md similarity index 100% rename from prismacloud/api/pccs/README.md rename to prismacloudapi/pccs/README.md diff --git a/prismacloud/api/pccs/__init__.py b/prismacloudapi/pccs/__init__.py similarity index 100% rename from prismacloud/api/pccs/__init__.py rename to prismacloudapi/pccs/__init__.py diff --git a/prismacloud/api/pccs/_checkov_version.py b/prismacloudapi/pccs/_checkov_version.py similarity index 100% rename from prismacloud/api/pccs/_checkov_version.py rename to prismacloudapi/pccs/_checkov_version.py diff --git a/prismacloud/api/pccs/_code_policies.py b/prismacloudapi/pccs/_code_policies.py similarity index 100% rename from prismacloud/api/pccs/_code_policies.py rename to prismacloudapi/pccs/_code_policies.py diff --git a/prismacloud/api/pccs/_errors.py b/prismacloudapi/pccs/_errors.py similarity index 92% rename from prismacloud/api/pccs/_errors.py rename to prismacloudapi/pccs/_errors.py index 4c1a856..56299cb 100644 --- a/prismacloud/api/pccs/_errors.py +++ b/prismacloudapi/pccs/_errors.py @@ -9,7 +9,7 @@ def errors_files_list(self, criteria): return self.execute_code_security('POST', 'code/api/v1/errors/files', body_params=criteria) def errors_file_list(self, criteria): - return self.execute_code_security('POST', 'code/api/v1/errors/file', body_params=criteria, paginated=True) + return self.execute_code_security_paginated('POST', 'code/api/v1/errors/file', body_params=criteria) def errors_list_last_authors(self, query_params=None): return self.execute_code_security('GET', 'code/api/v1/errors/gitBlameAuthors', query_params=query_params) diff --git a/prismacloud/api/pccs/_fixes.py b/prismacloudapi/pccs/_fixes.py similarity index 100% rename from prismacloud/api/pccs/_fixes.py rename to prismacloudapi/pccs/_fixes.py diff --git a/prismacloud/api/pccs/_packages.py b/prismacloudapi/pccs/_packages.py similarity index 100% rename from prismacloud/api/pccs/_packages.py rename to prismacloudapi/pccs/_packages.py diff --git a/prismacloud/api/pccs/_repositories.py b/prismacloudapi/pccs/_repositories.py similarity index 100% rename from prismacloud/api/pccs/_repositories.py rename to prismacloudapi/pccs/_repositories.py diff --git a/prismacloud/api/pccs/_rules.py b/prismacloudapi/pccs/_rules.py similarity index 100% rename from prismacloud/api/pccs/_rules.py rename to prismacloudapi/pccs/_rules.py diff --git a/prismacloud/api/pccs/_scans.py b/prismacloudapi/pccs/_scans.py similarity index 100% rename from prismacloud/api/pccs/_scans.py rename to prismacloudapi/pccs/_scans.py diff --git a/prismacloud/api/pccs/_suppressions.py b/prismacloudapi/pccs/_suppressions.py similarity index 100% rename from prismacloud/api/pccs/_suppressions.py rename to prismacloudapi/pccs/_suppressions.py diff --git a/prismacloudapi/pccs/pccs.py b/prismacloudapi/pccs/pccs.py new file mode 100644 index 0000000..e0a33aa --- /dev/null +++ b/prismacloudapi/pccs/pccs.py @@ -0,0 +1,104 @@ +""" Requests and Output """ + +import json +import time + +import requests + +class PrismaCloudAPIPCCSMixin(): + """ Requests and Output """ + + # pylint: disable=too-many-arguments,too-many-branches,too-many-locals,too-many-statements + def execute_code_security(self, action, endpoint, query_params=None, body_params=None, request_headers=None): + self.suppress_warnings_when_verify_false() + if not self.token: + self.login() + if int(time.time() - self.token_timer) > self.token_limit: + self.extend_login() + if body_params: + body_params_json = json.dumps(body_params) + else: + body_params_json = None + # Endpoints that return large numbers of results use a 'hasNext' key. + # Pagination is via query parameters for both GET and POST, and appears to be specific to "List File Errors - POST". + url = 'https://%s/%s' % (self.api, endpoint) + self.debug_print('API URL: %s' % url) + self.debug_print('API Headers: %s' % request_headers) + self.debug_print('API Query Params: %s' % query_params) + self.debug_print('API Body Params: %s' % body_params_json) + api_response = self.session.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) + self.debug_print('API Response Status Code: %s' % api_response.status_code) + self.debug_print('API Response Headers: (%s)' % api_response.headers) + if api_response.ok: + if not api_response.content: + return None + if api_response.headers.get('Content-Type') == 'application/x-gzip': + return api_response.content + if api_response.headers.get('Content-Type') == 'text/csv': + return api_response.content.decode('utf-8') + try: + result = api_response.json() + except ValueError: + self.logger.error('JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + self.error_and_exit(api_response.status_code, 'JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + return result + else: + self.logger.error('API: (%s) responded with a status of: (%s), with query: (%s) and body params: (%s)' % (url, api_response.status_code, query_params, body_params)) + self.error_and_exit(api_response.status_code, 'API: (%s) with query params: (%s) and body params: (%s) responded with an error and this response:\n%s' % (url, query_params, body_params, api_response.text)) + return None + + def execute_code_security_paginated(self, action, endpoint, query_params=None, body_params=None, request_headers=None): + self.suppress_warnings_when_verify_false() + if not self.token: + self.login() + if int(time.time() - self.token_timer) > self.token_limit: + self.extend_login() + if body_params: + body_params_json = json.dumps(body_params) + else: + body_params_json = None + # Endpoints that return large numbers of results use a 'hasNext' key. + # Pagination is via query parameters for both GET and POST, and appears to be specific to "List File Errors - POST". + offset = 0 + limit = 100 + more = False + while offset == 0 or more is True: + if int(time.time() - self.token_timer) > self.token_limit: + self.extend_login() + url = 'https://%s/%s?limit=%s&offset=%s' % (self.api, endpoint, limit, offset) + self.debug_print('API URL: %s' % url) + self.debug_print('API Headers: %s' % request_headers) + self.debug_print('API Query Params: %s' % query_params) + self.debug_print('API Body Params: %s' % body_params_json) + api_response = self.session.request(action, url, headers=request_headers, params=query_params, data=body_params_json, verify=self.verify, timeout=self.timeout) + self.debug_print('API Response Status Code: %s' % api_response.status_code) + self.debug_print('API Response Headers: (%s)' % api_response.headers) + if api_response.ok: + if not api_response.content: + return None + if api_response.headers.get('Content-Type') == 'application/x-gzip': + return api_response.content + if api_response.headers.get('Content-Type') == 'text/csv': + return api_response.content.decode('utf-8') + try: + result = api_response.json() + except ValueError: + self.logger.error('JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + self.error_and_exit(api_response.status_code, 'JSON raised ValueError, API: (%s) with query params: (%s) and body params: (%s) parsing response: (%s)' % (url, query_params, body_params, api_response.content)) + yield from result['data'] + if 'hasNext' in result: + self.debug_print('Retrieving Next Page of Results') + offset += limit + more = result['hasNext'] + else: + return + else: + self.logger.error('API: (%s) responded with a status of: (%s), with query: (%s) and body params: (%s)' % (url, api_response.status_code, query_params, body_params)) + self.error_and_exit(api_response.status_code, 'API: (%s) with query params: (%s) and body params: (%s) responded with an error and this response:\n%s' % (url, query_params, body_params, api_response.text)) + return + + # Exit handler (Error). + + @classmethod + def error_and_exit(cls, error_code, error_message='', system_message=''): + raise SystemExit('\n\nStatus Code: %s\n%s\n%s\n' % (error_code, error_message, system_message)) diff --git a/pyproject.toml b/pyproject.toml index 7fd26b9..dbef61d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,32 @@ [build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" \ No newline at end of file +requires = ["setuptools", "setuptools_scm", "setuptools_scm[toml]"] +build-backend = "setuptools.build_meta" + +[project] +name = "prismacloudapi" +authors = [ + { name = "Loic Jaquemet", email = "loic.jaquemet+python@gmail.com" }, + { name = "Tom Kishel", email = "tkishel@paloaltonetworks.com" } +] +maintainers = [{ name = "Loic Jaquemet", email = "loic.jaquemet+python@gmail.com" }] +license = { text = "License :: OSI Approved :: MIT License" } +description = "Prisma Cloud API SDK for Python - loic version" +readme = "README.md" +keywords = ["prisma", "cloud", "api", "prismacloud", "prismacloudapi"] +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Topic :: Utilities" +] +dependencies = ["requests"] +requires-python = ">=3.10" +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/trolldbois/prismacloud-api-python" +Download = "https://github.com/trolldbois/prismacloud-api-python/releases" +Original = "https://github.com/PaloAltoNetworks/prismacloud-api-python" + +[tool.setuptools_scm] + diff --git a/requirements.txt b/requirements.txt index 5f88e26..f229360 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1 @@ requests -update_checker \ No newline at end of file diff --git a/scripts/examples/pcs_vuln_container_with_cve_2022_22965.py b/scripts/examples/pcs_vuln_container_with_cve_2022_22965.py index 7b81c6e..a083c96 100644 --- a/scripts/examples/pcs_vuln_container_with_cve_2022_22965.py +++ b/scripts/examples/pcs_vuln_container_with_cve_2022_22965.py @@ -2,7 +2,7 @@ import os -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_account_groups_by_tags.py b/scripts/pcs_account_groups_by_tags.py index 8f4e8eb..12169c9 100644 --- a/scripts/pcs_account_groups_by_tags.py +++ b/scripts/pcs_account_groups_by_tags.py @@ -4,7 +4,7 @@ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_agentless_logs.py b/scripts/pcs_agentless_logs.py index 5447d7b..fa56573 100644 --- a/scripts/pcs_agentless_logs.py +++ b/scripts/pcs_agentless_logs.py @@ -1,7 +1,7 @@ """ Download and Save Agentless Logs """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_alert_rule_add_compliance_policies.py b/scripts/pcs_alert_rule_add_compliance_policies.py index c2c8aad..73fb91f 100644 --- a/scripts/pcs_alert_rule_add_compliance_policies.py +++ b/scripts/pcs_alert_rule_add_compliance_policies.py @@ -1,7 +1,7 @@ """ Add Policies to an alert rule based on compliance standard """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_alert_rule_export.py b/scripts/pcs_alert_rule_export.py index 7a4b218..42e3a90 100644 --- a/scripts/pcs_alert_rule_export.py +++ b/scripts/pcs_alert_rule_export.py @@ -1,7 +1,7 @@ """ Export Alert Rules """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_alert_rule_import.py b/scripts/pcs_alert_rule_import.py index 890bab3..41b2709 100644 --- a/scripts/pcs_alert_rule_import.py +++ b/scripts/pcs_alert_rule_import.py @@ -3,7 +3,7 @@ import json # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_alerts_read.py b/scripts/pcs_alerts_read.py index 336a067..1d87085 100644 --- a/scripts/pcs_alerts_read.py +++ b/scripts/pcs_alerts_read.py @@ -3,7 +3,7 @@ import json # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_apis_ingested.py b/scripts/pcs_apis_ingested.py index 0812e04..21f9ef1 100644 --- a/scripts/pcs_apis_ingested.py +++ b/scripts/pcs_apis_ingested.py @@ -1,7 +1,7 @@ """ Get a list of Alerts """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_cloud_account_import_azure.py b/scripts/pcs_cloud_account_import_azure.py index e51d8d3..2011fb3 100644 --- a/scripts/pcs_cloud_account_import_azure.py +++ b/scripts/pcs_cloud_account_import_azure.py @@ -1,7 +1,7 @@ """ Import Azure Accounts from a CSV file """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_cloud_account_inventory.py b/scripts/pcs_cloud_account_inventory.py index 76cc9e9..d1cc60d 100644 --- a/scripts/pcs_cloud_account_inventory.py +++ b/scripts/pcs_cloud_account_inventory.py @@ -4,7 +4,7 @@ import csv # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_cloud_discovery_defense_stats.py b/scripts/pcs_cloud_discovery_defense_stats.py index 4e73828..505a59f 100644 --- a/scripts/pcs_cloud_discovery_defense_stats.py +++ b/scripts/pcs_cloud_discovery_defense_stats.py @@ -1,7 +1,7 @@ """ Get statistics from cloud discovery """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_compliance_alerts_read.py b/scripts/pcs_compliance_alerts_read.py index e569fdc..e03d092 100644 --- a/scripts/pcs_compliance_alerts_read.py +++ b/scripts/pcs_compliance_alerts_read.py @@ -3,7 +3,7 @@ import json # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_compliance_export.py b/scripts/pcs_compliance_export.py index 63da79f..b07037f 100644 --- a/scripts/pcs_compliance_export.py +++ b/scripts/pcs_compliance_export.py @@ -1,7 +1,7 @@ """ Export a specific Compliance Standard """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_compliance_import.py b/scripts/pcs_compliance_import.py index c9d2c2d..9f0725a 100644 --- a/scripts/pcs_compliance_import.py +++ b/scripts/pcs_compliance_import.py @@ -7,7 +7,7 @@ import requests # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # TODO: Do not update policy.rule.name when policy.systemDefault == True ? diff --git a/scripts/pcs_compliance_uuid_read.py b/scripts/pcs_compliance_uuid_read.py index 62cdaba..3edb541 100644 --- a/scripts/pcs_compliance_uuid_read.py +++ b/scripts/pcs_compliance_uuid_read.py @@ -1,7 +1,7 @@ """ Get the UUID of a specific Compliance Standard (or Requirement or Section) """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_compute_container_observed_connections.py b/scripts/pcs_compute_container_observed_connections.py index b11cfde..e1b2082 100644 --- a/scripts/pcs_compute_container_observed_connections.py +++ b/scripts/pcs_compute_container_observed_connections.py @@ -3,7 +3,7 @@ import urllib.parse # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_compute_endpoint_client.py b/scripts/pcs_compute_endpoint_client.py index 7bf541e..35f41f2 100644 --- a/scripts/pcs_compute_endpoint_client.py +++ b/scripts/pcs_compute_endpoint_client.py @@ -4,7 +4,7 @@ from sys import exit as sys_exit, stderr, stdout # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_compute_forward_to_siem.py b/scripts/pcs_compute_forward_to_siem.py index 2b878ad..319586b 100644 --- a/scripts/pcs_compute_forward_to_siem.py +++ b/scripts/pcs_compute_forward_to_siem.py @@ -19,7 +19,7 @@ import requests # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_configure.py b/scripts/pcs_configure.py index 91f68e7..4c825e4 100644 --- a/scripts/pcs_configure.py +++ b/scripts/pcs_configure.py @@ -1,7 +1,7 @@ """ Configure """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_container_count.py b/scripts/pcs_container_count.py index 60e0b94..b94bb6f 100644 --- a/scripts/pcs_container_count.py +++ b/scripts/pcs_container_count.py @@ -1,7 +1,7 @@ """ Get a Count of Protected Containers """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_container_csp.py b/scripts/pcs_container_csp.py index b0fa28a..0315f86 100644 --- a/scripts/pcs_container_csp.py +++ b/scripts/pcs_container_csp.py @@ -3,7 +3,7 @@ import json # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_container_vulnerabilities_on_running_hosts.py b/scripts/pcs_container_vulnerabilities_on_running_hosts.py index 0b0d893..98f91dc 100644 --- a/scripts/pcs_container_vulnerabilities_on_running_hosts.py +++ b/scripts/pcs_container_vulnerabilities_on_running_hosts.py @@ -7,7 +7,7 @@ from dateutil import tz # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_container_vulnerabilities_read.py b/scripts/pcs_container_vulnerabilities_read.py index 632f610..d29ea28 100644 --- a/scripts/pcs_container_vulnerabilities_read.py +++ b/scripts/pcs_container_vulnerabilities_read.py @@ -3,7 +3,7 @@ import json # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_cs_errors_for_file.py b/scripts/pcs_cs_errors_for_file.py index 34d2c00..26b798f 100644 --- a/scripts/pcs_cs_errors_for_file.py +++ b/scripts/pcs_cs_errors_for_file.py @@ -1,7 +1,7 @@ """ Returns a list of potential Code Security policy violations for the specified file path """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_cs_repositories_read.py b/scripts/pcs_cs_repositories_read.py index 8cdbe50..f47081a 100644 --- a/scripts/pcs_cs_repositories_read.py +++ b/scripts/pcs_cs_repositories_read.py @@ -1,7 +1,7 @@ """ Returns a list of repositories that are integrated with Prisma Cloud Code Security """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_current_registry_scan.py b/scripts/pcs_current_registry_scan.py index a0b9312..bea57ac 100644 --- a/scripts/pcs_current_registry_scan.py +++ b/scripts/pcs_current_registry_scan.py @@ -7,7 +7,7 @@ import time # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_defender_report_by_cloud_account.py b/scripts/pcs_defender_report_by_cloud_account.py index c1b4427..fb0a5ac 100644 --- a/scripts/pcs_defender_report_by_cloud_account.py +++ b/scripts/pcs_defender_report_by_cloud_account.py @@ -1,7 +1,7 @@ """ Get a Count of Protected Containers """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_forensics_download.py b/scripts/pcs_forensics_download.py index ae22581..cc1f629 100644 --- a/scripts/pcs_forensics_download.py +++ b/scripts/pcs_forensics_download.py @@ -1,7 +1,7 @@ """ Download and Save Forensics """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_hosts_vulnerabilities_read.py b/scripts/pcs_hosts_vulnerabilities_read.py index bd49c2d..b1f6598 100644 --- a/scripts/pcs_hosts_vulnerabilities_read.py +++ b/scripts/pcs_hosts_vulnerabilities_read.py @@ -3,7 +3,7 @@ import json # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_images_packages_read.py b/scripts/pcs_images_packages_read.py index 31b06f9..e4557f9 100644 --- a/scripts/pcs_images_packages_read.py +++ b/scripts/pcs_images_packages_read.py @@ -3,7 +3,7 @@ from packaging import version # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_incident_archiver.py b/scripts/pcs_incident_archiver.py index db13f1f..781bff4 100644 --- a/scripts/pcs_incident_archiver.py +++ b/scripts/pcs_incident_archiver.py @@ -3,7 +3,7 @@ # See workflow in scripts/README.md # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_outdated_defenders.py b/scripts/pcs_outdated_defenders.py index d96fbf8..564853a 100644 --- a/scripts/pcs_outdated_defenders.py +++ b/scripts/pcs_outdated_defenders.py @@ -1,7 +1,7 @@ """ Get Outdated Defenders """ from packaging import version -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_policy_custom_export.py b/scripts/pcs_policy_custom_export.py index 5a4834b..167d859 100644 --- a/scripts/pcs_policy_custom_export.py +++ b/scripts/pcs_policy_custom_export.py @@ -1,7 +1,7 @@ """ Export Custom Policies """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_policy_custom_import.py b/scripts/pcs_policy_custom_import.py index 61308ab..d3cb3c3 100644 --- a/scripts/pcs_policy_custom_import.py +++ b/scripts/pcs_policy_custom_import.py @@ -6,7 +6,7 @@ import requests # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_policy_read.py b/scripts/pcs_policy_read.py index 8eb2d70..a07eff2 100644 --- a/scripts/pcs_policy_read.py +++ b/scripts/pcs_policy_read.py @@ -3,7 +3,7 @@ import json # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_policy_set_status.py b/scripts/pcs_policy_set_status.py index 30e8483..fce2b1a 100644 --- a/scripts/pcs_policy_set_status.py +++ b/scripts/pcs_policy_set_status.py @@ -3,7 +3,7 @@ import sys # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_posture_endpoint_client.py b/scripts/pcs_posture_endpoint_client.py index 5a38df9..b61cf14 100644 --- a/scripts/pcs_posture_endpoint_client.py +++ b/scripts/pcs_posture_endpoint_client.py @@ -4,7 +4,7 @@ from sys import exit as sys_exit, stderr, stdout # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_resources_export.py b/scripts/pcs_resources_export.py index 1cd686b..5f56d85 100644 --- a/scripts/pcs_resources_export.py +++ b/scripts/pcs_resources_export.py @@ -1,7 +1,7 @@ """ Get Resources """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_rotate_service_account_access_key.py b/scripts/pcs_rotate_service_account_access_key.py index 9ab811a..7176837 100644 --- a/scripts/pcs_rotate_service_account_access_key.py +++ b/scripts/pcs_rotate_service_account_access_key.py @@ -4,7 +4,7 @@ import time # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_rql_query.py b/scripts/pcs_rql_query.py index 2e54ee3..666413b 100644 --- a/scripts/pcs_rql_query.py +++ b/scripts/pcs_rql_query.py @@ -3,7 +3,7 @@ import json # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_script_example.py b/scripts/pcs_script_example.py index 76a57af..d77cc44 100644 --- a/scripts/pcs_script_example.py +++ b/scripts/pcs_script_example.py @@ -1,7 +1,7 @@ """ Example of Prisma Cloud (and Compute) API Access """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_sync_azure_accounts.py b/scripts/pcs_sync_azure_accounts.py index 93f2aef..8f5039e 100644 --- a/scripts/pcs_sync_azure_accounts.py +++ b/scripts/pcs_sync_azure_accounts.py @@ -3,7 +3,7 @@ import json # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_sync_registries.py b/scripts/pcs_sync_registries.py index e7095a2..5e6bc01 100644 --- a/scripts/pcs_sync_registries.py +++ b/scripts/pcs_sync_registries.py @@ -3,7 +3,7 @@ from operator import itemgetter # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_usage.py b/scripts/pcs_usage.py index 88eddaa..deefea4 100644 --- a/scripts/pcs_usage.py +++ b/scripts/pcs_usage.py @@ -1,7 +1,7 @@ """ Get Usage """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_user_import.py b/scripts/pcs_user_import.py index 9960419..4cd8915 100644 --- a/scripts/pcs_user_import.py +++ b/scripts/pcs_user_import.py @@ -1,7 +1,7 @@ """ Import Users from a CSV file """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_user_update.py b/scripts/pcs_user_update.py index 87efd93..d25d3f9 100644 --- a/scripts/pcs_user_update.py +++ b/scripts/pcs_user_update.py @@ -1,7 +1,7 @@ """ Update a User """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_vuln_container_locations.py b/scripts/pcs_vuln_container_locations.py index 80cbdfd..bb1140b 100644 --- a/scripts/pcs_vuln_container_locations.py +++ b/scripts/pcs_vuln_container_locations.py @@ -1,7 +1,7 @@ """ Get a list of vulnerable containers and their clusters """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility # --Configuration-- # diff --git a/scripts/pcs_week_alert_trend.py b/scripts/pcs_week_alert_trend.py index 7738193..705cf9b 100644 --- a/scripts/pcs_week_alert_trend.py +++ b/scripts/pcs_week_alert_trend.py @@ -1,7 +1,7 @@ """ Get Resources """ # pylint: disable=import-error -from prismacloud.api import pc_api, pc_utility +from prismacloudapi import pc_api, pc_utility from tabulate import tabulate import pandas as pd diff --git a/setup.py b/setup.py index 1218eb8..bbdee35 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,7 @@ -import importlib -import os -import setuptools +from setuptools import setup -with open('README.md', 'r') as fh: - long_description = fh.read() - -spec = importlib.util.spec_from_file_location( - 'prismacloud.api.version', os.path.join('prismacloud', 'api', 'version.py') -) - -mod = importlib.util.module_from_spec(spec) -spec.loader.exec_module(mod) -version = mod.version - -setuptools.setup( - name='prismacloud-api', - version=version, - author='Tom Kishel', - author_email='tkishel@paloaltonetworks.com', - description='Prisma Cloud API SDK for Python', - keywords="prisma cloud api", - long_description=long_description, - long_description_content_type='text/markdown', - url='https://github.com/PaloAltoNetworks/prismacloud-api-python', - packages=setuptools.find_namespace_packages(exclude=['scripts']), - classifiers=[ - 'Programming Language :: Python :: 3', - 'License :: OSI Approved :: MIT License', - 'Operating System :: OS Independent', - 'Topic :: Utilities' - ], - install_requires=[ - 'requests', - 'update_checker' - ], - python_requires='>=3.6' -) +if __name__ == "__main__": + setup( + name="prismacloudapi", + use_scm_version=True, + setup_requires=["setuptools_scm"]) diff --git a/tests/unit.py b/tests/unit.py index 54dc843..b41228c 100644 --- a/tests/unit.py +++ b/tests/unit.py @@ -5,7 +5,7 @@ import mock # pylint: disable=import-error -from prismacloud.api import pc_api +from prismacloudapi import pc_api class TestPrismaCloudAPI(unittest.TestCase): """ Unit Tests with Mocking """ @@ -51,7 +51,7 @@ class TestPrismaCloudAPI(unittest.TestCase): } # Decorator - @mock.patch('prismacloud.api.pc_utility.get_settings') + @mock.patch('prismacloudapi.pc_utility.get_settings') def test_pc_api_configure(self, get_settings): get_settings.return_value = self.SETTINGS settings = get_settings() @@ -60,7 +60,7 @@ def test_pc_api_configure(self, get_settings): # With def test_pc_api_current_user(self): - with mock.patch('prismacloud.api.PrismaCloudAPI.execute') as pc_api_execute: + with mock.patch('prismacloudapi.PrismaCloudAPI.execute') as pc_api_execute: pc_api_execute.return_value = self.USER_PROFILE result = pc_api.current_user() self.assertEqual('Example User', result['displayName'])