Palo Alto Networks AIOps
Palo Alto Networks Best Practice Assessment (BPA) analyzes NGFW and Panorama configurations and compares them to the best practices.
Utilities · Palo Alto Networks AIOps
Details
| ID | Palo Alto Networks AIOps |
|---|---|
| Provider | Palo Alto Networks |
| Category | Utilities |
| From Version | 6.9.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix Cloud Runtime Security XSIAM EDR Cortex Cloud |
README
Palo Alto Networks Best Practice Assessment (BPA) analyzes NGFW and Panorama configurations and compares them to the best practices.
This integration was integrated and tested with version from March 2024 of PaloAltoNetworksAIOps.
Configure Palo Alto Networks AIOps in Cortex
| Parameter | Required |
|---|---|
| Pan-OS/Panorama Server URL | True |
| Pan-OS/Panorama API Key | True |
| TSG ID | True |
| Client ID | True |
| Client Secret | True |
| Trust any certificate (not secure) | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
aiops-bpa-report-generate
Generates a bpa report. Steps -
- Get information about Pan-OS/Panorama device.
- Get configuration file of Pan-OS/Panorama. If the user provided an entry_id to a config file this step is skipped.
- Use the information retrieved above to generate a BPA report.
- During this process the API also generates a report_id for internal use.
Base Command
aiops-bpa-report-generate
Input
| Argument Name | Description | Required |
|---|---|---|
| entry_id | - Optional: Use this argument if you prefer to upload a configuration file instead of generating the report from Panorama/PAN-OS. - Entry_id from Cortex XSOAR War Room after uploading a file - should be a config file in xml format. - If you used this argument and the process failed or reached a timeout, make sure the config file is in xml format. |
Optional |
| requester_email | Requester email. | Required |
| requester_name | Requester name. | Required |
| interval_in_seconds | Interval for polling mechanism. Default is 30. | Optional |
| timeout | Timeout for downloading the file. Default is 600. | Optional |
| export_as_file | Whether to export the generated report as a file. Possible values are: true, false. Default is True. | Optional |
| show_in_context | Whether to show the report data inside the context. Possible values are: true, false. Default is False. | Optional |
Context Output
By default, there is no context output for this command.
When using show_in_context = True flag the generated report will be inserted to the context data.
Command example
!aiops-bpa-report-generate requester_email=testl@gmail.com requester_name=test
Human Readable Output
- Initiated
The report with id 7fec3669-c7bc-4113-b8b9-cae6a2aeb066 was sent successfully. Download in progress…
- If generation was successful
Generated a file with the relevant data and insert into context data if requested.
- If generation was unsuccessful
The report with id 7fec3669-c7bc-4113-b8b9-cae6a2aeb066 could not be generated- finished with an error.
- If timed out
Scheduled entry timed out.
This indicates that the configuration file is not in the correct format or that the timeout period is insufficient for generating the report
Configuration parameters
url— Pan-OS/Panorama Server URL (required)credentials— (required)tsg_id— TSG ID (required)credentials-aiops— Client ID (required)insecure— Trust any certificate (not secure)
Commands (2)
-
aiops-bpa-report-generateGenerates a bpa report. Steps: - Get configuration file of Pan-OS/Panorama. If the user provided an entry_id to a config file this step is skipped. - Use the information retrieved above to generate a BPA report. - During this process the API also generates a report_id.
-
aiops-polling-upload-reportPolling mechanism to upload report.
import io from typing import Any import demistomock as demisto # noqa: F401 import urllib3 from CommonServerPython import * # noqa: F401 from CommonServerUserPython import * # noqa urllib3.disable_warnings() """ GLOBAL VARIABLES """ INTERVAL_FOR_POLLING_DEFAULT = 30 TIMEOUT_FOR_POLLING_DEFAULT = 600 DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # ISO8601 format with UTC, default in XSOAR """ CLIENT CLASS """ class Client(BaseClient): def __init__(self, base_url, api_key, tsg_id, client_id, client_secret, verify=True, proxy=False, headers=None): super().__init__(base_url=base_url, verify=verify, proxy=proxy, headers=headers) self._api_key = api_key self._tsg_id = tsg_id self._client_id = client_id self._client_secret = client_secret self._access_token = None def generate_access_token_request(self): integration_context = get_integration_context() tsg_access_token = f"{self._tsg_id}.access_token" tsg_expiry_time = f"{self._tsg_id}.expiry_time" previous_token = integration_context.get(tsg_access_token) previous_token_expiry_time = integration_context.get(tsg_expiry_time) # type: ignore if previous_token and previous_token_expiry_time and previous_token_expiry_time > date_to_timestamp(datetime.now()): self._access_token = previous_token else: data = {"grant_type": "client_credentials", "scope": f"tsg_id:{self._tsg_id}"} headers = { "Content-Type": "application/x-www-form-urlencoded", "Accept": "application/json", } # Trying to be as accurate as possible with the time of the request expiry_time = date_to_timestamp(datetime.now(), date_format=DATE_FORMAT) try: res = self._http_request( method="POST", full_url="https://auth.apps.paloaltonetworks.com/auth/v1/oauth2/access_token", auth=(self._client_id, self._client_secret), resp_type="response", headers=headers, data=data, ) except DemistoException as e: raise DemistoException( f"Error occurred while creating an access token. Please check the instance configuration.\n\n{e}" ) try: res = res.json() except ValueError as exception: raise DemistoException(f"Failed to parse json object from response: {res.text}.\nError: {exception}") if access_token := res.get("access_token"): expiry_time += res.get("expires_in", 0) * 1000 new_token = {tsg_access_token: access_token, tsg_expiry_time: expiry_time} # stores received token and expiration time in the integration context set_integration_context(new_token) self._access_token = new_token.get(tsg_access_token) else: raise DemistoException( "Error occurred while creating an access token. Access token field has not" " found in the response data. Please check the instance configuration.\n" ) def get_info_about_device_request(self): headers = {"Content-Type": "application/xml"} params = assign_params(type="op", cmd="<show><system><info></info></system></show>", key=self._api_key) try: response = self._http_request("GET", "/api", params=params, headers=headers, resp_type="xml") except DemistoException as e: raise DemistoException(f"Could not get info about device. Request finished with an error {e}.") formated_xml = adjust_xml_format(response.text, "system") return formated_xml def get_config_file_request(self): headers = {"Content-Type": "application/xml"} params = assign_params(type="config", action="show", key=self._api_key) try: response = self._http_request("GET", "/api", params=params, headers=headers, resp_type="xml") except DemistoException as e: raise DemistoException(f"Could not get config file. Request finished with an error {e}.") formated_xml = adjust_xml_format(response.text, "config") return formated_xml def generate_bpa_report_request(self, requester_email, requester_name, system_info): body = { "requester-email": requester_email, "requester-name": requester_name, "serial": system_info.get("serial", None), "version": system_info.get("sw-version", None), "model": system_info.get("model", None), "family": system_info.get("family", None), } headers = { "Content-Type": "application/json", "Accept": "application/json", "Authorization": f"Bearer {self._access_token}", } res = self._http_request( method="POST", full_url="https://api.stratacloud.paloaltonetworks.com/aiops/bpa/v1/requests", headers=headers, json_data=body, ) upload_url = res.get("upload-url", None) report_id = res.get("id", None) if upload_url and report_id: return upload_url, report_id raise DemistoException(f"Response not in format, can not find uploaded-url or report id. With response {res}.") def config_file_to_report_request(self, upload_url, config_in_binary): headers = {"Content-Type": "application/octet-stream", "Accept": "*/*", "Authorization": f"Bearer {self._access_token}"} res = self._http_request( method="PUT", full_url=upload_url, headers=headers, data=config_in_binary, empty_valid_codes=[200], return_empty_response=True, ) return res def check_upload_status_request(self, report_id): headers = {"Accept": "*/*", "Authorization": f"Bearer {self._access_token}"} res = self._http_request( method="GET", full_url=f"https://api.stratacloud.paloaltonetworks.com/aiops/bpa/v1/jobs/{report_id}", headers=headers ) status = res.get("status") if not status: raise DemistoException("Missing upload status, Error: parse Error.") return status def download_bpa_request(self, report_id): headers = {"Accept": "application/json", "Authorization": f"Bearer {self._access_token}"} res = self._http_request( method="GET", full_url=f"https://api.stratacloud.paloaltonetworks.com/aiops/bpa/v1/reports/{report_id}", headers=headers, ) url = res.get("download-url") if not url: raise DemistoException("Missing download-url, Error: parse Error.") return url def data_of_download_bpa_request(self, downloaded_BPA_url): headers = {"Authorization": f"Bearer {self._access_token}"} res = self._http_request(method="GET", full_url=downloaded_BPA_url, headers=headers) return res """ HELPER FUNCTIONS """ def adjust_xml_format(xml_string, new_root_tag): root = ET.fromstring(xml_string) sub_tags = root.find(f".//{new_root_tag}") if not sub_tags: raise DemistoException( f"Request Succeeded, A parse error occurred- could not find {new_root_tag} tag to adjust to AIOps API." ) attributes = " ".join([f'{k}="{v}"' for k, v in sub_tags.attrib.items()]) new_xml = f"<{new_root_tag} {attributes}>" for child in sub_tags: # type: ignore new_xml += ET.tostring(child, encoding="unicode") new_xml += f"</{new_root_tag}>" return new_xml def get_values_from_xml(xml_string, tags): result = [] root = ET.fromstring(xml_string) for tag in tags: try: result.append(root.find(tag).text) # type: ignore except Exception as e: raise DemistoException(f"Could not find the required tags in the System file. Error: {e}") return result def convert_config_to_bytes(config_file, origin_flag): if origin_flag == "User": try: get_file_path_res = demisto.getFilePath(config_file) file_path = get_file_path_res.pop("path") file_bytes: bytes = b"" with open(file_path, "rb") as f: file_bytes = f.read() return file_bytes except Exception as e: raise DemistoException( f"The config file upload was unsuccessful or the file could not be converted. With error: {e}." ) else: try: # Add xml tag to xml xml_header = '<?xml version="1.0"?>' result = f"{xml_header}\n {config_file}" sio_xml = io.StringIO(result) xml_in_bytes = sio_xml.read().encode() return xml_in_bytes except Exception: raise DemistoException("The downloaded config file from Panorama/Pan-os could not be converted.") def convert_response_for_hr(response_json): converted_array = [] check_category_options = ["device", "service_health", "objects", "network", "policies"] # Get best_practices elements (only warnings and notes) best_practices = response_json.get("best_practices", {}) for category in check_category_options: category_objects = best_practices.get(category, None) if category_objects: for key, value in category_objects.items(): if value: warnings = value[0].get("warnings") notes = value[0].get("notes") for warning in warnings: warning["check_type"] = "warning" warning["check_feature"] = key warning["check_category"] = category converted_array.append(warning) for note in notes: note["check_type"] = "note" note["check_feature"] = key note["check_category"] = category converted_array.append(note) return converted_array def create_response(client, report_id, show_in_context, export_as_file, upload_status): downloaded_BPA_url = client.download_bpa_request(report_id) downloaded_BPA_json = client.data_of_download_bpa_request(downloaded_BPA_url) converted_json = convert_response_for_hr(downloaded_BPA_json) human_readable_markdown = create_markdown(converted_json) response = [] # CommandResults depends on show_in_context arg if show_in_context: context_json = [{"report_id": report_id, "report_status": upload_status, "data": converted_json}] response.append( CommandResults( outputs_prefix="AiOps.BPAReport", outputs_key_field="report_id", outputs=context_json, raw_response=downloaded_BPA_json, readable_output=human_readable_markdown, ) ) else: response.append(CommandResults(raw_response=downloaded_BPA_json, readable_output=human_readable_markdown)) # Insert the markdown into a file if export_as_file: response.append(fileResult(f"report-id-{report_id}.md", human_readable_markdown)) return response def create_markdown(original_dict): headers = [ "check_id", "check_category", "check_feature", "check_message", "check_name", "check_passed", "check_type", "check_severity", ] return tableToMarkdown( "BPA results:", original_dict, headers=headers, removeNull=True, headerTransform=string_to_table_header ) """ COMMAND FUNCTIONS """ def test_module(client: Client) -> str: message: str = "" try: client.generate_access_token_request() except DemistoException as e: if "access token" in e.message or "Forbidden" in e.message or "Authorization" in e.message: raise DemistoException( f"Authorization Error: make sure your tsg_id, client_id, client_secret are correctly set. With error {e}" ) else: raise try: client.get_info_about_device_request() message = "ok" except Exception as e: raise DemistoException(f"Authorization Error: make sure your server_url and API_key are correctly set. With error {e}") return message def generate_report_command(client: Client, args: dict[str, Any]): client.generate_access_token_request() config_file_from_user = args.get("entry_id") requester_email = args.get("requester_email", "NoEmail") requester_name = args.get("requester_name") export_as_file = argToBoolean(args.get("export_as_file", True)) show_in_context = argToBoolean(args.get("show_in_context", False)) # Get info about device - system info if "@" not in requester_email: raise DemistoException(f"Invalid email {requester_email}, please make sure it is a valid email.") system_info_xml = client.get_info_about_device_request() tags = ["family", "model", "serial", "sw-version"] xml_tags_values = get_values_from_xml(system_info_xml, tags) if config_file_from_user: config_in_binary = convert_config_to_bytes(config_file_from_user, "User") # Get info configurations and convert to format if user didn't upload a config file elif config_file := client.get_config_file_request(): config_in_binary = convert_config_to_bytes(config_file, "Download") else: raise DemistoException("Can not upload a config file since it was not provided.") upload_url, report_id = client.generate_bpa_report_request(requester_email, requester_name, dict(zip(tags, xml_tags_values))) client.config_file_to_report_request(upload_url, config_in_binary) return_results( polling_until_upload_report_command( { "report_id": report_id, "export_as_file": export_as_file, "show_in_context": show_in_context, "hide_polling_output": True, "first_round": True, }, client, ) ) @polling_function( name="aiops-polling-upload-report", interval=arg_to_number(demisto.args().get("interval_in_seconds", INTERVAL_FOR_POLLING_DEFAULT)), # type: ignore timeout=arg_to_number(demisto.args().get("timeout", TIMEOUT_FOR_POLLING_DEFAULT)), # type: ignore requires_polling_arg=False, poll_message="", ) def polling_until_upload_report_command(args: dict[str, Any], client: Client) -> PollResult: client.generate_access_token_request() report_id = args.get("report_id") export_as_file = argToBoolean(args.get("export_as_file", True)) show_in_context = argToBoolean(args.get("show_in_context", False)) first_round = argToBoolean(args.get("first_round", False)) upload_status = client.check_upload_status_request(report_id) if upload_status == "COMPLETED_WITH_SUCCESS": response = create_response(client, report_id, show_in_context, export_as_file, upload_status) return PollResult( response=response, continue_to_poll=False, ) elif upload_status == "UPLOAD_INITIATED": results = CommandResults(readable_output="Polling job failed.") if first_round: return PollResult( response=results, continue_to_poll=True, args_for_next_run={ "report_id": report_id, "export_as_file": export_as_file, "show_in_context": show_in_context, "hide_polling_output": True, }, partial_result=CommandResults( readable_output=f"The report with id {report_id} was sent successfully. Download in progress..." ), ) else: return PollResult( response=results, continue_to_poll=True, args_for_next_run={ "report_id": report_id, "export_as_file": export_as_file, "show_in_context": show_in_context, "hide_polling_output": True, }, ) elif upload_status == "COMPLETED_WITH_ERROR": fail_output = [{"report_id": report_id, "report_status": upload_status}] return PollResult( response=CommandResults( outputs_prefix="AiOps.BPAReport", outputs_key_field="report_id", outputs=fail_output, raw_response=fail_output, readable_output=f"The report with id {report_id} could not be generated- finished with an error.", ), continue_to_poll=False, ) else: return PollResult( continue_to_poll=True, args_for_next_run={ "report_id": report_id, "hide_polling_output": True, "export_as_file": export_as_file, "show_in_context": show_in_context, }, response=None, ) """ MAIN FUNCTION """ def main() -> None: command = demisto.command() args = demisto.args() params = demisto.params() verify_certificate = not params.get("insecure", False) base_url = params.get("url") api_key = params.get("credentials", {}).get("password") tsg_id = params.get("tsg_id") client_id = params.get("credentials-aiops", {}).get("identifier") client_secret = params.get("credentials-aiops", {}).get("password") proxy = params.get("proxy", False) demisto.debug(f"Command being called is {command}") try: client = Client( base_url=base_url, api_key=api_key, tsg_id=tsg_id, client_id=client_id, client_secret=client_secret, verify=verify_certificate, proxy=proxy, ) if command == "test-module": return_results(test_module(client)) elif command == "aiops-bpa-report-generate": generate_report_command(client, args) elif command == "aiops-polling-upload-report": return_results(polling_until_upload_report_command(args, client)) else: raise NotImplementedError(f"command {command} is not implemented.") except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{e!s}") """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): main()