import json import sys from base64 import b64encode from typing import Any import demistomock as demisto # noqa: F401 import requests from CommonServerPython import * # noqa: F401 from netmiko import Netmiko """ Common setup """ # server param is mandatory, no need to check if present HOSTNAME = demisto.params()["server"] """ SSH integration setup """ sshConfigured = False # Others are not mandatory as user may choose to only use XML API commands and not SSH CREDS = demisto.params().get("Username", {}) USERNAME = CREDS.get("identifier") PASSWORD = CREDS.get("password") SSHPORT = demisto.params().get("sshport") panos = {} # Does user intend to leverage SSH commands if USERNAME and PASSWORD and SSHPORT: sshConfigured = True panos = {"device_type": "paloalto_panos", "ip": HOSTNAME, "username": USERNAME, "password": PASSWORD, "port": SSHPORT} def panos_connect(net_connect: Netmiko = None): # type: ignore[valid-type] try: if not net_connect: net_connect = Netmiko(**panos) prompt = net_connect.find_prompt() finally: if net_connect: net_connect.disconnect() return prompt def panos_ssh(cmd: str, net_connect: Netmiko = None): # type: ignore[valid-type] result_cmd = "" if sshConfigured: """ Run any command """ # execute command try: if not net_connect: net_connect = Netmiko(**panos) # Sometimes returns "debug", and will not wait for results result_cmd = net_connect.send_command_timing(cmd) # type: ignore[assignment] # This command ensures the first has finished result_cmd += net_connect.send_command_timing("\n") # type: ignore[operator] finally: if net_connect: net_connect.disconnect() # Remove newlines and carriage returns return result_cmd.replace("\n", " ").replace("\r", "") else: raise Exception("You must configure the SSH integration parameters to use this command.") def prisma_access_cli_command(): if sshConfigured: cmd = demisto.args().get("cmd") sshRes = panos_ssh(cmd) md = "### Prisma Access CLI Results\n" + sshRes ec = {"PrismaAccess.CLICommand": {"Command": cmd, "Results": sshRes}} demisto.results( { "Type": entryTypes["note"], "ContentsFormat": formats["text"], "Contents": sshRes, "ReadableContentsFormat": formats["markdown"], "HumanReadable": md, "EntryContext": ec, } ) else: raise Exception("You must configure the SSH integration parameters to use this command.") def prisma_access_query(): query = demisto.args().get("query") cmd = f"debug plugins cloud_services gpcs query {query}" sshRes = panos_ssh(cmd) jsonStartPos = sshRes.find('{"@status') if jsonStartPos < 0: return_outputs(f"Prisma Access returned non-JSON:\n{sshRes}") else: res = json.loads(sshRes[jsonStartPos:]) if res["@status"] == "success": data = res.get("result", {}).get("entry", {}) md = tableToMarkdown("Prisma Access Query Results", data) ec = {"PrismaAccess.QueryResults": data} contents = data return_outputs(md, ec, contents) else: resultsJsonAsText = json.dumps(res, indent=4) return_outputs(resultsJsonAsText, resultsJsonAsText, resultsJsonAsText) def prisma_access_active_users(): limit = demisto.args().get("limit", 20) cmd = f"debug plugins cloud_services gpcs query querystring limit={limit} action getGPaaSActiveUsers" sshRes = panos_ssh(cmd) jsonStartPos = sshRes.find('{"@status') if jsonStartPos < 0: return_outputs(f"Prisma Access returned non-JSON:\n{sshRes}") else: res = json.loads(sshRes[jsonStartPos:]) if res["@status"] == "success": data = res.get("result", {}).get("entry", {}) md = tableToMarkdown("Prisma Access Active Users", data) ec = {"PrismaAccess.ActiveUsers": data} contents = data return_outputs(md, ec, contents) else: resultsJsonAsText = json.dumps(res, indent=4) return_outputs(resultsJsonAsText, resultsJsonAsText, resultsJsonAsText) """ API integration setup """ # disable insecure warnings requests.packages.urllib3.disable_warnings() # type: ignore[attr-defined] # pylint: disable=no-member """ GLOBALS """ apiConfigured = False # Others are not mandatory as user may choose to only use XML API commands and not SSH KEY = secret_key = demisto.params().get("credentials_key", {}).get("password") or demisto.params().get("key", {}) PORT = demisto.params().get("port") # Does user intend to leverage SSH commands if KEY and PORT: apiConfigured = True URL = "https://" + demisto.params()["server"].rstrip("/:") + ":" + PORT + "/api/" API_KEY = str(KEY) USE_SSL = not demisto.params().get("insecure") # determine a vsys or a device-group VSYS = demisto.params().get("vsys") if demisto.args() and demisto.args().get("device-group", None): DEVICE_GROUP = demisto.args().get("device-group") else: DEVICE_GROUP = demisto.params().get("device_group", None) # configuration check if DEVICE_GROUP and VSYS: return_error("Cannot configure both vsys and Device group. Set vsys for firewall, set Device group for Panorama.") if not DEVICE_GROUP and not VSYS: return_error("Set vsys for firewall or Device group for Panorama.") # setting security xpath relevant to FW or panorama management if DEVICE_GROUP: device_group_shared = DEVICE_GROUP.lower() if device_group_shared == "shared": XPATH_SECURITY_RULES = "/config/shared/" DEVICE_GROUP = device_group_shared else: XPATH_SECURITY_RULES = "/config/devices/entry/device-group/entry[@name='" + DEVICE_GROUP + "']/" else: XPATH_SECURITY_RULES = "/config/devices/entry/vsys/entry[@name='" + VSYS + "']/rulebase/security/rules/entry" # setting objects xpath relevant to FW or panorama management if DEVICE_GROUP: device_group_shared = DEVICE_GROUP.lower() if DEVICE_GROUP == "shared": XPATH_OBJECTS = "/config/shared/" DEVICE_GROUP = device_group_shared else: XPATH_OBJECTS = "/config/devices/entry/device-group/entry[@name='" + DEVICE_GROUP + "']/" else: XPATH_OBJECTS = "/config/devices/entry/vsys/entry[@name='" + VSYS + "']/" PAN_OS_ERROR_DICT = { "1": "Unknown command - The specific config or operational command is not recognized.", "2": "Internal errors - Check with technical support when seeing these errors.", "3": "Internal errors - Check with technical support when seeing these errors.", "4": "Internal errors - Check with technical support when seeing these errors.", "5": "Internal errors - Check with technical support when seeing these errors.", "6": "Bad Xpath -The xpath specified in one or more attributes of the command is invalid." "Check the API browser for proper xpath values.", "7": "Object not present - Object specified by the xpath is not present. For example," "entry[@name=value] where no object with name value is present.", "8": "Object not unique - For commands that operate on a single object, the specified object is not unique.", "10": "Reference count not zero - Object cannot be deleted as there are other objects that refer to it." "For example, address object still in use in policy.", "11": "Internal error - Check with technical support when seeing these errors.", "12": "Invalid object - Xpath or element values provided are not complete.", "14": "Operation not possible - Operation is allowed but not possible in this case." "For example, moving a rule up one position when it is already at the top.", "15": "Operation denied - Operation is allowed. For example, Admin not allowed to delete own account," "Running a command that is not allowed on a passive device.", "16": "Unauthorized -The API role does not have access rights to run this query.", "17": "Invalid command -Invalid command or parameters.", "18": "Malformed command - The XML is malformed.", # 19,20: success "21": "Internal error - Check with technical support when seeing these errors.", "22": "Session timed out - The session for this query timed out.", } class PAN_OS_Not_Found(Exception): """PAN-OS Error.""" def http_request(uri: str, method: str, headers: dict = {}, body: dict = {}, params: dict = {}, files=None) -> Any: """ Makes an API call with the given arguments """ result = requests.request( method, uri, headers=headers, data=body, verify=USE_SSL, # pylint: disable=E0606 params=params, files=files, ) if result.status_code < 200 or result.status_code >= 300: raise Exception("Request Failed. with status: " + str(result.status_code) + ". Reason is: " + str(result.reason)) # if pcap download if params.get("type") == "export": return result json_result = json.loads(xml2json(result.text)) # handle non success if json_result["response"]["@status"] != "success": if "msg" in json_result["response"] and "line" in json_result["response"]["msg"]: # catch non existing object error and display a meaningful message if json_result["response"]["msg"]["line"] == "No such node": raise Exception("Object was not found, verify that the name is correct and that the instance was committed.") # catch urlfiltering error and display a meaningful message elif str(json_result["response"]["msg"]["line"]).find("test -> url") != -1: raise Exception( "The URL filtering license is either expired or not active. Please contact your PAN-OS representative." ) # catch non valid jobID errors and display a meaningful message elif ( isinstance(json_result["response"]["msg"]["line"], str) and json_result["response"]["msg"]["line"].find("job") != -1 and ( json_result["response"]["msg"]["line"].find("not found") != -1 or json_result["response"]["msg"]["line"].find("No such query job") ) != -1 ): raise Exception("Invalid Job ID error: " + json_result["response"]["msg"]["line"]) # catch already at the top/bottom error for rules and return this as an entry.note elif str(json_result["response"]["msg"]["line"]).find("already at the") != -1: demisto.results("Rule " + str(json_result["response"]["msg"]["line"])) sys.exit(0) # catch already registered ip tags and return this as an entry.note elif str(json_result["response"]["msg"]["line"]).find("already exists, ignore") != -1: if isinstance(json_result["response"]["msg"]["line"]["uid-response"]["payload"]["register"]["entry"], list): ips = [ o["@ip"] for o in json_result["response"]["msg"]["line"]["uid-response"]["payload"]["register"]["entry"] ] else: ips = json_result["response"]["msg"]["line"]["uid-response"]["payload"]["register"]["entry"]["@ip"] demisto.results("IP " + str(ips) + " already exist in the tag. All submitted IPs were not registered to the tag.") sys.exit(0) # catch timed out log queries and return this as an entry.note elif str(json_result["response"]["msg"]["line"]).find("Query timed out") != -1: demisto.results(str(json_result["response"]["msg"]["line"]) + ". Rerun the query.") sys.exit(0) if "@code" in json_result["response"]: raise Exception( "Request Failed.\nStatus code: " + str(json_result["response"]["@code"]) + "\nWith message: " + str(json_result["response"]["msg"]["line"]) ) else: raise Exception("Request Failed.\n" + str(json_result["response"])) # handle @code if "response" in json_result and "@code" in json_result["response"]: if json_result["response"]["@code"] in PAN_OS_ERROR_DICT: error_message = "Request Failed.\n" + PAN_OS_ERROR_DICT[json_result["response"]["@code"]] if json_result["response"]["@code"] == "7" and DEVICE_GROUP: device_group_names = get_device_groups_names() if DEVICE_GROUP not in device_group_names: error_message += ( f'\nDevice Group: {DEVICE_GROUP} does not exist.' f' The available Device Groups for this instance:' f' {", ".join(device_group_names)}.' ) raise PAN_OS_Not_Found(error_message) if json_result["response"]["@code"] not in ["19", "20"]: # error code non exist in dict and not of success if "msg" in json_result["response"]: raise Exception( "Request Failed.\nStatus code: " + str(json_result["response"]["@code"]) + "\nWith message: " + str(json_result["response"]["msg"]) ) else: raise Exception("Request Failed.\n" + str(json_result["response"])) return json_result def prisma_access_test(): """ test module """ # Test API connection only if user configured it if apiConfigured: try: params = { "type": "op", "cmd": "", "key": API_KEY, # pylint: disable=E0606 } http_request( URL, # pylint: disable=E0606 "GET", params=params, ) if DEVICE_GROUP and DEVICE_GROUP != "shared": device_group_test() except Exception as ex: raise type(ex)("PAN-OS XML API Test:\n" + str(ex)) # Test SSH connection only if user configured it if sshConfigured: try: panos_connect() except Exception as ex: raise type(ex)("PAN-OS SSH CLI Test:\n" + str(ex)) # If reached this point, all ok demisto.results("ok") def get_device_groups_names(): """ Get device group names in the Panorama """ params = {"action": "get", "type": "config", "xpath": "/config/devices/entry/device-group/entry", "key": API_KEY} result = http_request(URL, "GET", params=params) device_groups = result["response"]["result"]["entry"] device_group_names = [] if isinstance(device_groups, dict): # only one device group in the panorama device_group_names.append(device_groups.get("@name")) else: for device_group in device_groups: device_group_names.append(device_group.get("@name")) return device_group_names def device_group_test(): """ Test module for the Device group specified """ device_group_names = get_device_groups_names() if DEVICE_GROUP not in device_group_names: raise Exception( f'Device Group: {DEVICE_GROUP} does not exist.' f' The available Device Groups for this instance: {", ".join(device_group_names)}.' ) @logger def prisma_access_logout_user(computer: str, domain: str, user: str, tenant: str) -> dict[str, str]: if apiConfigured: xmlComputer = f"{b64encode(computer.encode('utf8')).decode('utf8')}" if computer else "" b64User = (b64encode(user.encode("utf8"))).decode("utf8") cmd = "" if domain: cmd = f""" {xmlComputer}{domain}{b64User} """ else: cmd = f""" {xmlComputer}{b64User} """ if tenant: tenant_entry = f"" cmd = cmd.replace("", f"{tenant_entry}").replace( "", "" ) params = {"type": "op", "key": API_KEY, "cmd": cmd} result = http_request(URL, "GET", params=params) return result else: raise Exception("You must configure the PAN-OS API Key and Port parameters to use this command.") def prisma_access_logout_user_command(): computer = demisto.args().get("computer", "") domain = demisto.args().get("domain", "") user = demisto.args().get("user", "") tenant = demisto.args().get("tenant_name", "") result = prisma_access_logout_user(computer, domain, user, tenant) if "result" in result["response"] and result["response"]["@status"] == "success": res = result["response"].get("result", "") hr = "### Prisma Access Logout Results:\n" + json.dumps(res, indent=4) demisto.results( { "Type": entryTypes["note"], "ContentsFormat": formats["json"], "Contents": res, "ReadableContentsFormat": formats["markdown"], "HumanReadable": hr, "EntryContext": {"PrismaAccess.LogoutUser": res}, } ) else: demisto.results(result) def main(): LOG(f"Command being called is: {demisto.command()}") try: # Remove proxy if not set to true in params handle_proxy() if demisto.command() == "test-module": prisma_access_test() elif demisto.command() == "prisma-access-logout-user": prisma_access_logout_user_command() elif demisto.command() == "prisma-access-query": prisma_access_query() elif demisto.command() == "prisma-access-active-users": prisma_access_active_users() elif demisto.command() == "prisma-access-cli-command": prisma_access_cli_command() else: raise NotImplementedError(f"Command {demisto.command()} was not implemented.") except Exception as err: return_error(str(err)) finally: LOG.print_log() if __name__ in ["__builtin__", "builtins"]: main()