Prisma Access
Integrate with Prisma Access to monitor the status of the Service, alert and take actions.
Network Security · Palo Alto Networks - Strata Cloud Manager
Details
| ID | Prisma Access |
|---|---|
| Provider | Palo Alto Networks |
| Category | Network Security |
| From Version | 5.0.0 |
| Docker Image | demisto/netmiko:1.0.0.9067966 |
| Supported Modules | Agentix Cloud Runtime Security Cloud Posture Security XSIAM EDR Cortex Cloud |
README
Prisma Access Integration
Integrate with Prisma Access to monitor the status of the Service, alert and take actions.
The integration uses both the Panorama XML API and SSH into the PAN-OS CLI.
Common parameters
The Server Host or IP parameter is required by both.
SSH connection
The following commands require the SSH access to be configured:
- prisma-access-active-users
- prisma-access-cli-command
- prisma-access-query
The SSH connection requires the SSH Credentials for CLI, Password and SSH Port are provided.
SSH credentials should be your username and password for the PAN-OS CLI - they can be tested using a standalone SSH client to verify that you are able to connect to the CLI on the SSH port.
API connection
The following commands require the API access to be configured:
- prisma-access-logout-user
The API connection requires the API Port and API Key parameters as well as a Device Group or Vsys.
This integration was integrated and tested with version 9.0.7 of Prisma Access
Configure Prisma Access in Cortex
| Parameter | Description | Required |
|---|---|---|
| server | Server Host or IP (e.g., 10.1.1.9 or panorama.my.domain) | True |
| port | API Port (e.g 443) | False |
| key | API Key | False |
| insecure | Trust any certificate (not secure) | False |
| proxy | Use system proxy settings | False |
| device_group | Device group - Panorama instances only (write shared for Shared location) | False |
| vsys | Vsys - Firewall instances only | False |
| sshport | SSH Port | False |
| Username | SSH Credentials for CLI | 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.
prisma-access-logout-user
Force logout a specific user from Prisma Access
Base Command
prisma-access-logout-user
Input
| Argument Name | Description | Required |
|---|---|---|
| user | Username to logout. (Without domain name - e.g. jsmith) | Required |
| domain | Domain name of the user to logout. | Required |
| computer | Computer name to logout. | Required |
| tenant_name | The tenant name. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| PrismaAccess.LogoutUser | unknown | LogoutUser command results |
Command Example
!prisma-access-logout-user user="jsmith" domain="acme" computer="jsmithPC"
prisma-access-query
Run a query via the Prisma Access CLI
Base Command
prisma-access-query
Input
| Argument Name | Description | Required |
|---|---|---|
| query | Query to run. Example input: querystring limit=2000 action getGPaaSLast90DaysUniqueUsers | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| PrismaAccess.QueryResults | unknown | Query results |
Command Example
!prisma-access-query query="querystring limit=2 action getGPaaSActiveUsers"
prisma-access-cli-command (deprecated)
Run a custom CLI command on Prisma Access
Base Command
prisma-access-cli-command
Input
| Argument Name | Description | Required |
|---|---|---|
| cmd | CLI command to run (e.g. debug plugins cloud_services gpcs query querystring limit=9000 action getGPaaSLast90DaysUniqueUsers) | Required |
Context Output
There is no context output for this command.
Command Example
!prisma-access-cli-command cmd="show system info | match hostname"
prisma-access-active-users (deprecated)
Query currently active users.
Base Command
prisma-access-active-users
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | Maximum number of entries to return. Default is 20. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| PrismaAccess.ActiveUsers | unknown | Active Users on Prisma Access |
Command Example
!prisma-access-active-users limit=10
Configuration parameters
server— Server Host or IP (e.g., 10.1.1.9 or panorama.my.domain) (required)port— API Port (e.g 443)key— API Keycredentials_key—insecure— Trust any certificate (not secure)proxy— Use system proxy settingsdevice_group— Device group - Panorama instances only (write shared for Shared location)vsys— Vsys - Firewall instances onlysshport— SSH PortUsername— SSH Credentials for CLI
Commands (4)
-
prisma-access-active-usersDeprecatedQuery currently active users.
-
prisma-access-cli-commandDeprecatedRun a custom CLI command on Prisma Access.
-
prisma-access-logout-userForce logout a specific user from Prisma Access.
-
prisma-access-queryDeprecatedRun a query via the Prisma Access CLI.
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": "<show><system><info></info></system></show>", "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"<computer>{b64encode(computer.encode('utf8')).decode('utf8')}</computer>" if computer else "" b64User = (b64encode(user.encode("utf8"))).decode("utf8") cmd = "" if domain: cmd = f"""<request><plugins><cloud_services><gpcs> <logout_mobile_user><gateway>{xmlComputer}<domain>{domain}</domain><user>{b64User}</user></gateway></logout_mobile_user> </gpcs></cloud_services></plugins></request>""" else: cmd = f"""<request><plugins><cloud_services><gpcs> <logout_mobile_user><gateway>{xmlComputer}<user>{b64User}</user></gateway></logout_mobile_user> </gpcs></cloud_services></plugins></request>""" if tenant: tenant_entry = f"<multi-tenant><tenant-name><entry name='{tenant}'></entry></tenant-name>" cmd = cmd.replace("<gpcs>", f"<gpcs>{tenant_entry}").replace( "</logout_mobile_user>", "</logout_mobile_user></multi-tenant>" ) 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()