from typing import Dict, Tuple, List import urllib3 from CommonServerPython import * # Disable insecure warnings urllib3.disable_warnings() ''' GLOBALS/PARAMS ''' BPA_HOST = 'https://bpa.paloaltonetworks.com' BPA_VERSION = 'v1' BPA_URL = BPA_HOST + '/api/' + BPA_VERSION + '/' DOWNLOADED_REPORT_NAME_SUFFIX = '_BPA-report.zip' class LightPanoramaClient(BaseClient): ''' This is a client for Panorama API, used by integration commands to issue requests to Panorama API, not the BPA service. ''' def __init__(self, server, port, api_key, verify, proxy): if port is None: super().__init__(server + '/', verify) else: super().__init__(server.rstrip('/:') + ':' + port + '/', verify) self.api_key = api_key if proxy: self.proxies = handle_proxy() else: self.proxies = {} def simple_op_request(self, cmd): params = { 'type': 'op', 'cmd': cmd, 'key': self.api_key } result = self._http_request( 'POST', 'api', params=params, resp_type='text', proxies=self.proxies ) return result @logger def get_system_time(self): return self.simple_op_request('') @logger def get_license(self): return self.simple_op_request('') @logger def get_system_info(self): return self.simple_op_request('') @logger def get_running_config(self): params = { 'type': 'config', 'action': 'show', 'key': self.api_key } result = self._http_request( 'POST', 'api', params=params, resp_type='text', proxies=self.proxies ) return result class Client(BaseClient): """ Client to use in the BPA integration. This client issues requests to the BPA service, and not Panorama. """ def __init__(self, bpa_token: str, verify: bool, proxy: bool): headers = {'Authorization': f'Token {bpa_token}'} super().__init__(base_url=BPA_URL, verify=verify, headers=headers, proxy=proxy) self.token = bpa_token if proxy: self.proxies = handle_proxy() else: self.proxies = {} def get_documentation_request(self): response = self._http_request('GET', 'documentation/', proxies=self.proxies) return response def submit_task_request(self, running_config, system_info, license_info, system_time, generate_zip_bundle, timeout) -> Dict: data = { 'xml': running_config, 'system_info': system_info, 'license_info': license_info, 'system_time': system_time, 'generate_zip_bundle': generate_zip_bundle } response = self._http_request('POST', 'create/', data=data, proxies=self.proxies, timeout=timeout) return response def get_results_request(self, task_id: str): response = self._http_request('GET', f'results/{task_id}/', proxies=self.proxies) return response def get_download_results_request(self, task_id: str) -> bytes: response = self._http_request('GET', f'results/{task_id}/download', resp_type='content', proxies=self.proxies) return response def create_output(doc: dict): doc_output = {} for key in doc.keys(): doc_output[string_to_context_key(key)] = doc.get(key) return doc_output def get_documentation_command(client: Client, args: Dict) -> Tuple[str, Dict, Dict]: raw = client.get_documentation_request() if not raw: raise Exception('Failed getting documentation from BPA') filter_by_ids = args.get('doc_ids') if filter_by_ids: output = [] old_output = [] # keep old output format in order to not break backwards compatibility id_list = filter_by_ids.split(',') for doc in raw: if str(doc.get('doc_id')) in id_list: output.append(create_output(doc)) old_output.append(doc) else: output = [create_output(doc) for doc in raw] old_output = raw entry_context = { 'PAN-OS-BPA.Documentation.Document': output, 'PAN-OS-BPA.Documentation': old_output # Keep old output path in order to not break backwards compatibility. } human_readable = tableToMarkdown('BPA documentation', output) return human_readable, entry_context, raw def submit_task_command(client: Client, panorama: LightPanoramaClient, args: Dict) -> Tuple[str, Dict, Dict]: try: running_config = panorama.get_running_config() system_info = panorama.get_system_info() license_info = panorama.get_license() system_time = panorama.get_system_time() except Exception: raise Exception('Failed getting response from Panorama') generate_zip_bundle = args.get('generate_zip_bundle') timeout = int(args.get('timeout', '120')) raw = client.submit_task_request(running_config, system_info, license_info, system_time, generate_zip_bundle, timeout) task_id = raw.get('task_id', '') human_readable = f'Submitted BPA job ID: {task_id}' entry_context = {'PAN-OS-BPA.SubmittedJob(val.JobID && val.JobID === obj.JobID)': {'JobID': task_id}} return human_readable, entry_context, raw def transform_check(check, feature, category): # safe to shallow clone since it is a shallow object transformed_check = check.copy() transformed_check['check_category'] = category transformed_check['check_feature'] = feature return transformed_check def get_checks_from_feature(feature, feature_name, category): notes_checks = feature.get('notes', []) warnings_checks = feature.get('warnings', []) return [transform_check(c, feature_name, category) for c in notes_checks + warnings_checks] def get_results_command(client: Client, args: Dict): task_id = args.get('task_id', '') filter_by_check_id = args.get('check_id', '').split(',') if args.get('check_id') else [] filter_by_check_name = args.get('check_name', '').split(',') if args.get('check_name') else [] raw: Dict = client.get_results_request(task_id) status = raw.get('status') results = raw.get('results', {}) exclude_passed_checks = args.get('exclude_passed_checks') == "true" if not status: raise Exception("Invalid response from BPA") job_checks: List[Dict] = [] if status == 'invalid': raise Exception("Job ID not valid or doesn't exist") if status == 'complete': bpa = results.get('bpa', {}) if not bpa: raise Exception("Invalid response from BPA") for category_name, features in bpa.items(): for feature_name, feature_contents in features.items(): if not feature_contents: # Empty list, no checks continue checks = get_checks_from_feature(feature_contents[0], feature_name, category_name) if exclude_passed_checks: job_checks.extend([check for check in checks if not check.get('check_passed')]) elif filter_by_check_id or filter_by_check_name: job_checks.extend([check for check in checks if str(check.get('check_id')) in filter_by_check_id or check.get('check_name') in filter_by_check_name]) else: job_checks.extend(checks) download_url = results.get('download_url') # check that a report was generated, and can be downloaded if download_url: download_report_handler(client, task_id) context = {'PAN-OS-BPA.JobResults(val.JobID && val.JobID === obj.JobID)': { 'JobID': task_id, 'Checks': job_checks, 'Status': status }} headers = ['check_id', 'check_category', 'check_feature', 'check_message', 'check_name', 'check_passed', 'check_type', 'check_severity'] human_readable = tableToMarkdown('BPA Results', job_checks, headers=headers, headerTransform=string_to_table_header) return human_readable, context, results def download_report_handler(client: Client, task_id): downloaded_report = client.get_download_results_request(task_id) demisto.results(fileResult(task_id + DOWNLOADED_REPORT_NAME_SUFFIX, downloaded_report, entryTypes['entryInfoFile'])) def test_module(client, panorama): client.get_documentation_request() panorama.get_system_time() demisto.results('ok') return '', None, None def main(): """ PARSE AND VALIDATE INTEGRATION PARAMS """ params = demisto.params() panorama_server = params.get('server') panorama_port = params.get('port', None) panorama_api_key = params.get('key_creds', {}).get('password') or params.get('key') bpa_token = params.get('token_creds', {}).get('password') or params.get('token') verify = not params.get('insecure', False) proxy = params.get('proxy') try: client = Client(bpa_token, verify, proxy) panorama = LightPanoramaClient(panorama_server, panorama_port, panorama_api_key, verify, proxy) command = demisto.command() LOG(f'Command being called is {command}.') if command == 'pan-os-bpa-submit-job': return_outputs(*submit_task_command(client, panorama, demisto.args())) elif command == 'pan-os-bpa-get-job-results': return_outputs(*get_results_command(client, demisto.args())) elif command == 'pan-os-get-documentation': return_outputs(*get_documentation_command(client, demisto.args())) elif command == 'test-module': return_outputs(*test_module(client, panorama)) else: raise NotImplementedError(f'Command "{command}" is not implemented.') except Exception as err: return_error(str(err)) if __name__ in ['__main__', 'builtin', 'builtins']: main()