Tufin

Retrieve and analyze network access controls across Tufin-managed firewalls, SDN, and public cloud to identify vulnerable access paths of an attack.

Network Security · Tufin

Details

IDTufin
ProviderTurn River Capital
CategoryNetwork Security
From Version5.0.0
Docker Imagedemisto/netutils:1.0.0.10187688
Supported ModulesAgentix XSIAM

README

Overview


Retrieve and analyze network access controls across Tufin-managed firewalls, SDN, and public cloud to identify vulnerable access paths of an attack
This integration was integrated and tested with version 19.3 of Tufin Orchestration Suite

Tufin Playbook


Use Cases


Gather network intelligence from SecureTrack and SecureApp, perform topology queries in SecureTrack, and submit change tickets from SecureChange.

Configure Tufin on Cortex XSOAR


  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Tufin.
  3. Click Add instance to create and configure a new integration instance.
    • Name: a textual name for the integration instance.
    • TOS IP or FQDN
    • TOS User Credentials
    • Trust any certificate (not secure)
    • Use system proxy settings
    • Maximum number of rules returned from device durring a policy search
  4. Click Test to validate the URLs, token, and connection.

Fetched Incidents Data


Commands


You can execute these commands from the Cortex XSOAR 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.

  1. tufin-search-topology
  2. tufin-search-topology-image
  3. tufin-object-resolve
  4. tufin-policy-search
  5. tufin-get-zone-for-ip
  6. tufin-submit-change-request
  7. tufin-search-devices
  8. tufin-get-change-info
  9. tufin-search-applications
  10. tufin-search-application-connections

1. tufin-search-topology


Search the Tufin Topology Map

Base Command

tufin-search-topology

Input
Argument Name Description Required
source Source address/addresses (may contain multiple, comma separated values) e.g. 192.168.100.32 or 192.168.100.32/32,192.168.100.33 Required
destination Destination address/addresses (may contain multiple, comma separated values) e.g. 192.168.100.32 or 192.168.100.32/32,192.168.100.33 Required
service Service parameter can be a port (for example, “tcp:80”, “any”) or an application (for example, “Skype”, “Facebook”). Optional
Context Output
Path Type Description
Tufin.Topology.TrafficAllowed boolean Traffic Permitted
Tufin.Topology.TrafficDevices string List of devices in path
Command Example

!tufin-search-topology destination=10.2.2.0/24 source=192.168.60.0/24

Human Readable Output

image

2. tufin-search-topology-image


Search the Tufin Topology Map, returning an image

Base Command

tufin-search-topology-image

Input
Argument Name Description Required
source Source address/addresses (may contain multiple, comma separated values) Required
destination Destination address/addresses (may contain multiple, comma separated values) Required
service Service parameter can be a port (for example, “tcp:80”, “any”) or an application (for example, “Skype”, “Facebook”). Optional
Context Output

There is no context output for this command.

Command Example

!tufin-search-topology-image destination=10.2.2.0/24 source=192.168.60.0/24

Human Readable Output

image

3. tufin-object-resolve


Resolve IP address to Network Object

Base Command

tufin-object-resolve

Input
Argument Name Description Required
ip IP Address to Resolve to Network Object Required
Context Output
Path Type Description
Tufin.ObjectResolve.NumberOfObjects number Number of objects that resolve to given IP address.
Command Example

!tufin-object-resolve ip=10.3.3.3

Human Readable Output

image

4. tufin-policy-search


Search the policies of all devices managed by Tufin

Base Command

tufin-policy-search

Input
Argument Name Description Required
search The text format is for a field is fieldname:text for example source:192.168.1.1 or bareword for free text search. See the search info documentation in Securetrack Policy Browser page for more information. Required
Context Output
Path Type Description
Tufin.Policysearch.NumberRulesFound number Number of rules found via search
Command Example

!tufin-policy-search search="source:192.168.1.1"

Human Readable Output

image

5. tufin-get-zone-for-ip


Match the IP address to the assigned Tufin Zone

Base Command

tufin-get-zone-for-ip

Input
Argument Name Description Required
ip IP Address Required
Context Output
Path Type Description
Tufin.Zone.ID string Tufin Zone ID
Tufin.Zone.Name unknown Tufin Zone Name
Command Example

!tufin-get-zone-for-ip ip=10.10.12.1

Human Readable Output

image

6. tufin-submit-change-request


Submit a change request to SecureChange

Base Command

tufin-submit-change-request

Input
Argument Name Description Required
request-type Request Type Required
priority Request Priority Required
source Source or Target Required
destination Destination (Mandatory for FW Change) Optional
protocol Protocol (Mandatory for FW Change) Optional
port Port (Mandatory for FW Change) Optional
action Action (Mandatory for FW Change) Optional
comment Comment Optional
subject Ticket Subject Required
Context Output
Path Type Description
Tufin.Request.Status unknown Status of the request submission
Command Example

!tufin-submit-change-request request-type="Decommission Request" priority=High source=192.168.1.1 subject="This host is infected with ransomware"

Human Readable Output

image

7. tufin-search-devices


Search SecureTrack devices

Base Command

tufin-search-devices

Input
Argument Name Description Required
name Device name Optional
ip Device IP Optional
vendor Device vendor Optional
model Device model Optional
Context Output
Path Type Description
Tufin.Device.ID unknown Device ID
Tufin.Device.Name unknown Device name
Tufin.Device.Vendor unknown Device vendor
Tufin.Device.Model unknown Device model
Tufin.Device.IP unknown Device IP
Command Example

!tufin-search-devices vendor=Cisco

Human Readable Output

image

8. tufin-get-change-info


Get information on a SecureChange Ticket (Ticket ID retrieved from Tufin UI)

Base Command

tufin-get-change-info

Input
Argument Name Description Required
ticket-id SecureChange Ticket ID Required
Context Output
Path Type Description
Tufin.Ticket.ID unknown Ticket ID
Tufin.Ticket.Subject unknown Ticket subject
Tufin.Ticket.Priority unknown Ticket priority
Tufin.Ticket.Status unknown Ticket status
Tufin.Ticket.Requester unknown Ticket requester
Tufin.Ticket.WorkflowID unknown Ticket workflow ID
Tufin.Ticket.WorkflowName unknown Ticket workflow name
Tufin.Ticket.CurrentStep unknown Ticket current step
Command Example

!tufin-get-change-info ticket-id=250

Human Readable Output

image

9. tufin-search-applications


Search SecureApp applications

Base Command

tufin-search-applications

Input
Argument Name Description Required
name Application name Optional
Context Output
Path Type Description
Tufin.App.ID unknown Application ID
Tufin.App.Name unknown Application name
Tufin.App.Status unknown Application status
Tufin.App.Decommissioned unknown Is the application decommissioned
Tufin.App.OwnerID unknown Application owner ID
Tufin.App.OwnerName unknown Application owner name
Tufin.App.Comments unknown Application comments
Command Example

!tufin-search-applications name="3Rivers"

Human Readable Output

image

10. tufin-search-application-connections


Get SecureApp application connections

Base Command

tufin-search-application-connections

Input
Argument Name Description Required
application-id Application ID Required
Context Output
Path Type Description
Tufin.AppConnections.ID unknown Connection ID
Tufin.AppConnections.Name unknown Connection name
Tufin.AppConnections.Status unknown Connection status
Tufin.AppConnections.Source.ID unknown Connection source ID
Tufin.AppConnections.Source.Type unknown Connection source type
Tufin.AppConnections.Source.Name unknown Connection source name
Tufin.AppConnections.Destination.ID unknown Connection destination ID
Tufin.AppConnections.Destination.Type unknown Connection destination type
Tufin.AppConnections.Destination.Name unknown Connection destination name
Tufin.AppConnections.Service.ID unknown Connection service ID
Tufin.AppConnections.Service.Name unknown Connection service name
Tufin.AppConnections.Comment unknown Connection comment
Tufin.AppConnections.ApplicationID unknown Application ID
Command Example

!tufin-search-application-connections app_id=215

Human Readable Output

image

Troubleshooting


Contact Tufin support via the Tufin User Portal, or by going to https://www.tufin.com/support

Configuration parameters

  • SecureTrack-Server — SecureTrack IP or FQDN (required)
  • SecureTrack-User — SecureTrack User Credentials (required)
  • SecureChange-Server — SecureChange IP or FQDN
  • SecureChange-User — SecureChange User Credentials
  • SecureApp-Server — SecureApp IP or FQDN
  • SecureApp-User — SecureApp User Credentials
  • unsecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • MaxRules — Maximum number of rules returned from device durring a policy search

Commands (10)

  • tufin-get-change-info

    Get information on a SecureChange Ticket (ID retrieved from SecureChange UI).

  • tufin-get-zone-for-ip

    Match the IP address to the assigned Tufin Zone.

  • tufin-object-resolve

    Resolve IP address to Network Object.

  • tufin-policy-search

    Search the policies of all devices managed by Tufin.

  • tufin-search-application-connections

    Get SecureApp application connections.

  • tufin-search-applications

    Search SecureApp applications.

  • tufin-search-devices

    Search SecureTrack devices.

  • tufin-search-topology

    Search the Tufin Topology Map.

  • tufin-search-topology-image

    Search the Tufin Topology Map, returning an image.

  • tufin-submit-change-request

    Submit a change request to SecureChange.

import demistomock as demisto
from CommonServerPython import *
import json
import requests
import socket
from typing import Any
from netaddr import IPNetwork, IPAddress

# Disable insecure warnings
import urllib3

urllib3.disable_warnings()

"""
Templates for change requests

Change these templates if you have customized your Firewall Change Request or Server Decommission Request worflows to match
your workflow.  To view the JSON structure of your customized workflows, create a sample ticket then view the data via the
API: https://<SecureChange IP Address>/securechangeworkflow/api/securechange/tickets/<tickt_ID>.json
"""

FW_CHANGE_REQ = json.loads("""{ "ticket": { "subject": "", "priority": "", "workflow": { "name": "Firewall Change Request",
                           "uses_topology": true }, "steps": { "step": [ { "name": "Submit Access Request", "tasks": { "task":
                           { "fields": { "field": { "@xsi.type": "multi_access_request", "name": "Required Access",
                           "access_request": { "use_topology": true, "targets": { "target": { "@type": "ANY" } },
                           "users": { "user": [ "Any" ] }, "sources": { "source": [ { "@type": "IP", "ip_address": "",
                           "netmask": "255.255.255.255", "cidr": 32 } ] }, "destinations": { "destination": [ { "@type": "IP",
                           "ip_address": "", "netmask": "255.255.255.255", "cidr": 32 } ] }, "services": { "service":
                            [ { "@type": "PROTOCOL", "protocol": "", "port": 0 } ] }, "action": "" } } } } } } ] },
                            "comments": "" } }""")
SERVER_DECOM_REQ = json.loads("""{ "ticket": { "subject": "", "priority": "", "workflow": { "name":
                              "Server Decommission Request", "uses_topology": false }, "steps": { "step": [ {
                              "name": "Server Decommission Request", "tasks": { "task": { "fields": { "field": { "@xsi.type":
                              "multi_server_decommission_request", "name": "Request verification",
                              "server_decommission_request": { "servers": { "server": { "@type": "IP", "ip_address": "",
                              "netmask": "255.255.255.255", "cidr": 32 } }, "targets": { "target": { "@type": "ANY" } },
                              "comment": "" } } } } } } ] }, "comments": "" }}""")

# remove proxy if not set to true in params
if not demisto.params().get("proxy"):
    # Remove proxy environment variables if they exist
    for proxy_var in ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"]:
        os.environ.pop(proxy_var, None)


def tos_request(tos_app, req_type, path, params=None, headers=None, data=None):
    """Function to access TOS via REST API"""
    if headers is None:
        headers = {"accept": "application/json", "content-type": "application/json", "cache-control": "no-cache"}

    # Get Configuration
    tos_ip = ""
    tos_user = ""
    tos_pass = ""
    if tos_app == "st":
        tos_ip = demisto.params()["SecureTrack-Server"]
        tos_user = demisto.params()["SecureTrack-User"]["identifier"]
        tos_pass = demisto.params()["SecureTrack-User"]["password"]
    elif tos_app == "sc":
        tos_ip = demisto.params()["SecureChange-Server"]
        tos_user = demisto.params()["SecureChange-User"]["identifier"]
        tos_pass = demisto.params()["SecureChange-User"]["password"]
    elif tos_app == "sa":
        tos_ip = demisto.params()["SecureApp-Server"]
        tos_user = demisto.params()["SecureApp-User"]["identifier"]
        tos_pass = demisto.params()["SecureApp-User"]["password"]
    verify_ssl = not demisto.params().get("unsecure", False)
    url = "https://" + tos_ip + path

    # Go do
    if req_type.upper() == "GET":
        try:
            res = requests.get(url, params=params, headers=headers, auth=(tos_user, tos_pass), verify=verify_ssl)
        except requests.exceptions.RequestException as e:
            return_error(str(e))

        # Check output
        if res.status_code == 200 or res.status_code == 201:
            try:
                return res.json()
            except json.decoder.JSONDecodeError:
                return res.content
        else:
            if res.status_code == 401:
                return_error("TOS Reached, Auth Failed. Please check your credentials")
                return None
            else:
                return_error(f"Error {res.status_code} Reaching {res.url} to TOS: {res.reason}")
                return None
    elif req_type.upper() == "POST":
        try:
            res = requests.post(url, data=data, params=params, headers=headers, auth=(tos_user, tos_pass), verify=verify_ssl)
        except requests.exceptions.RequestException as e:
            return_error(str(e))

        # Check output
        if res.status_code == 200 or res.status_code == 201:
            try:
                return res.json()
            except json.decoder.JSONDecodeError:
                return res.content
        else:
            if res.status_code == 401:
                return_error("TOS Reached, Auth Failed. Please check your credentials")
                return None
            else:
                return_error(f"Error {res.status_code} Reaching {res.url} to TOS: {res.reason}")
                return None
    return None


def valid_ip(ipa):
    # ipaddress module not installed by default, using this approach
    try:
        socket.inet_aton(ipa)
        return True
    except OSError:
        return False


def path_finder(querystring):
    # Define the basic output for the function, augmenting later with TOS data
    entry: dict[str, Any] = {
        "Type": entryTypes["note"],
        "Contents": "",
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "",
        "EntryContext": {},
    }

    # Ask TOS for the path
    o = tos_request("st", "GET", "/securetrack/api/topology/path", querystring)

    # Verify the data and return
    try:
        entry["EntryContext"]["Tufin.Topology.TrafficAllowed"] = o["path_calc_results"]["traffic_allowed"]
        entry["EntryContext"]["Tufin.Topology.TrafficDevices"] = [d["name"] for d in o["path_calc_results"]["device_info"]]
        entry["Contents"] = o["path_calc_results"]["device_info"]
        entry["HumanReadable"] = tableToMarkdown(
            "Tufin Topology Search for {} to {} via Service {}. Traffic is {}".format(
                querystring["src"],
                querystring["dst"],
                querystring["service"],
                ("**Denied**", "**Allowed**")[o["path_calc_results"]["traffic_allowed"]],
            ),
            {
                "Start": querystring["src"],
                "Devices in Path": "-->".join(
                    ["**" + d["name"] + "**" + " ({})".format(d["vendor"]) for d in o["path_calc_results"]["device_info"]]
                ),
                "End": querystring["dst"],
            },
            ["Start", "Devices in Path", "End"],
        )

    except KeyError:
        return_error("Unknown Output Returned")
    # Send back to Demisto inside function
    return entry


def path_finder_command():
    """Sample query: querystring = {'src':'10.80.80.0','dst':'172.16.200.80',
    'service':'tcp:22','includeIncompletePaths':'true'}"""
    # Build the query from user input
    querystring = {
        "src": demisto.args()["source"],
        "dst": demisto.args()["destination"],
        "service": demisto.args().get("service", "Any"),
        "includeIncompletePaths": "true",
    }
    e = path_finder(querystring)
    demisto.results(e)


def path_finder_image(querystring):
    entry = {
        "Type": entryTypes["note"],
        "Contents": "",
        "ContentsFormat": formats["text"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "",
        "EntryContext": {},
    }

    try:
        headers = {"accept": "image/png", "content-type": "application/json", "cache-control": "no-cache"}
        img = tos_request("st", "GET", "/securetrack/api/topology/path_image", querystring, headers)
        # simple check if we have an image or error message.
        if len(img) > 20:
            # Send back to Demisto inside function
            return fileResult("topo.png", img, entryTypes["image"])
        else:
            entry["HumanReadable"] = "No Valid Path Found"
            entry["Contents"] = "No Valid Path Found"
            # Send back to Demisto inside function
            return entry
    except Exception as e:
        return_error(f"Error Running Query: {e}")


def path_finder_image_command():
    """Sample query: querystring = {'src':'10.80.80.0','dst':'172.16.200.80',
    'service':'tcp:80','includeIncompletePaths':'true','displayBlockedStatus':'true'}"""
    querystring = {
        "src": demisto.args()["source"],
        "dst": demisto.args()["destination"],
        "service": demisto.args().get("service", "Any"),
        "includeIncompletePaths": "true",
        "displayBlockedStatus": "true",
    }
    e = path_finder_image(querystring)
    demisto.results(e)


def device_name(devices, device_id):
    return [e["name"] + " ({} {})".format(e["vendor"], e["model"]) for e in devices if int(e["id"]) == int(device_id)][0]


def object_lookup(querystring):
    entry: dict[str, Any] = {
        "Type": entryTypes["note"],
        "Contents": "",
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "",
        "EntryContext": {},
    }

    return_json: dict[str, list] = {"objects": []}

    o = tos_request("st", "GET", "/securetrack/api/network_objects/search", querystring)

    # Validate result
    try:
        total = int(o["network_objects"]["count"])
    except KeyError:
        total = 0

    if total > 0:
        device_json = tos_request("st", "GET", "/securetrack/api/devices")["devices"]["device"]
        objs = o["network_objects"]["network_object"]
        if not isinstance(o["network_objects"]["network_object"], list):
            objs = [objs]
        for obj in objs:
            # display_name device_id
            return_json["objects"].append(
                {
                    "object_name": obj["display_name"],
                    "device": device_name(device_json, obj["device_id"]),
                    "comment": obj["comment"],
                }
            )
    else:
        entry["HumanReadable"] = "No Results"
        entry["EntryContext"]["Tufin.ObjectResolve.NumberOfObjects"] = 0
        return entry

    # Return to Demisto
    entry["Contents"] = json.dumps(return_json)
    entry["EntryContext"]["Tufin.ObjectResolve.NumberOfObjects"] = total
    entry["HumanReadable"] = tableToMarkdown(
        "Object Lookup for {}".format(querystring["exact_subnet"]),
        return_json["objects"],
        ["object_name", "device", "comment"],
        underscoreToCamelCase,
        removeNull=True,
    )
    # Send back to Demisto inside function
    return entry


def object_lookup_command():
    """Sample query: querystring = {'filter':'subnet','count':'50','exact_subnet':'1.1.1.1'}"""
    querystring = {"filter": "subnet", "count": "50", "exact_subnet": demisto.args()["ip"]}
    if not valid_ip(querystring["exact_subnet"]):
        return_error("Invalid IP Address")
        return False

    e = object_lookup(querystring)
    demisto.results(e)
    return None


def policy_search(querystring, max_rules_per_device=100):
    """Search policy across all devices.  See docs for syntax"""
    u = "/securetrack/api/rule_search"
    entry: dict[str, Any] = {
        "Type": entryTypes["note"],
        "Contents": "",
        "ContentsFormat": formats["json"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "",
        "EntryContext": {},
    }

    matches = tos_request("st", "GET", u, querystring)
    search_devices = [e["device_id"] for e in matches["device_list"]["device"] if int(e["rule_count"]) > 0]

    if not len(search_devices):
        entry["HumanReadable"] = "No Results Found"
        entry["EntryContext"]["Tufin.Policysearch.NumberRulesFound"] = 0
        # Send back to Demisto inside function
        return entry
    else:
        rule_total = 0
        querystring["count"] = max_rules_per_device
        querystring["start"] = 0
        rule_return = []
        device_json = tos_request("st", "GET", "/securetrack/api/devices")["devices"]["device"]
        for d in search_devices:
            rules = tos_request("st", "GET", u + f"/{d}", querystring)
            # If no matches(there should be) just break the iteration
            if rules["rules"]["count"] == 0:
                break

            current_device = device_name(device_json, d)

            for rule in rules["rules"]["rule"]:
                rule_total = rule_total + 1
                rule_return.append(
                    {
                        "Device": current_device,
                        "Source": [d["display_name"] for d in rule["src_network"]],
                        "Source Service": [d["display_name"] for d in rule["src_service"]],
                        "Destination": [d["display_name"] for d in rule["dst_network"]],
                        "Destination Service": [d["display_name"] for d in rule["dst_service"]],
                        "Action": rule["action"],
                    }
                )
        # Send back to Demisto
        entry["Contents"] = json.dumps(rule_return)
        entry["EntryContext"]["Tufin.Policysearch.NumberRulesFound"] = rule_total
        entry["HumanReadable"] = tableToMarkdown(
            "Policy Search Results for {}".format(querystring["search_text"]),
            rule_return,
            ["Device", "Source", "Source Service", "Destination", "Destination Service", "Action"],
            removeNull=True,
        )
        # Send back to Demisto inside function
        return entry


def policy_search_command():
    max_rules_per_device = demisto.params()["MaxRules"]
    querystring = {"search_text": demisto.args()["search"]}
    e = policy_search(querystring, max_rules_per_device)
    demisto.results(e)


def zone_match(ipaddr):
    """Find the zone for the given IP address"""
    entry: dict[str, Any] = {
        "Type": entryTypes["note"],
        "Contents": "",
        "ContentsFormat": formats["text"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "",
        "EntryContext": {},
    }
    try:
        zone_list = tos_request("st", "GET", "/securetrack/api/zones/")
        for zone in zone_list["zones"]["zone"]:
            zone_subnets = tos_request("st", "GET", f"/securetrack/api/zones/{zone['id']}/entries")
            zone.update(zone_subnets)
            for subnet in zone_subnets["zone_entries"]["zone_entry"]:
                ipnet = "{}/{}".format(subnet["ip"], subnet["prefix"])
                if IPAddress(ipaddr) in IPNetwork(ipnet):
                    z = {}
                    z["Name"] = zone["name"]
                    z["ID"] = int(zone["id"])
                    entry["EntryContext"]["Tufin.Zone"] = [z]
                    entry["Contents"] = zone
                    entry["HumanReadable"] = tableToMarkdown(
                        f"Tufin Zone Search for {ipaddr}",
                        {"Name": zone["name"], "ID": zone["id"]},
                        ["Name", "ID"],
                        removeNull=True,
                    )
                    return entry
    except Exception as e:
        return_error(f"Error retrieving zone: {str(e)}")
    entry["EntryContext"]["Tufin.Zones"] = [{"Name": "None", "ID": "None"}]
    entry["Contents"] = "Not Found"
    entry["HumanReadable"] = tableToMarkdown(
        f"Tufin Zone Search for {ipaddr}", {"Name": "Not Found", "ID": "0"}, ["Name", "ID"], removeNull=True
    )
    return entry


def zone_match_command():
    ipaddr = demisto.getArg("ip")
    e = zone_match(ipaddr)
    demisto.results(e)


def change_req(req_type, subj, priority, src, dst="", proto="", port="", action="", comment=""):
    """Submit a change request to SecureChange"""
    entry: dict[str, Any] = {
        "Type": entryTypes["note"],
        "Contents": "",
        "ContentsFormat": formats["text"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "",
        "EntryContext": {},
    }
    try:
        if req_type.lower() == "firewall change request":
            # Check for valid input
            if (
                dst == ""
                or proto == ""
                or port == ""
                or action == ""
                or dst is None
                or proto is None
                or port is None
                or action is None
            ):
                return_error("""Request Type, Subject, Priority, Source, Destination, Protocol,
                             Port and Action parameters are mandatory for this request type""")
            if priority.capitalize() not in ["Critical", "High", "Normal", "Low"]:
                return_error("Priority must be Critical, High, Normal or Low")
            if proto.upper() not in ["TCP", "UDP"]:
                return_error("Protocol must be TCP or UDP")
            if action.capitalize() not in ["Accept", "Drop", "Remove"]:
                return_error("Action must be Accept, Drop or Remove")
            if not port.isdigit():
                return_error("Port must be an integer")
            # Build change request JSON
            req = FW_CHANGE_REQ
            req["ticket"]["subject"] = subj
            req["ticket"]["priority"] = priority.capitalize()
            (
                req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"]["access_request"]["sources"]["source"][0][
                    "ip_address"
                ]
            ) = src
            (
                req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"]["access_request"]["destinations"][
                    "destination"
                ][0]["ip_address"]
            ) = dst
            (
                req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"]["access_request"]["services"]["service"][0][
                    "protocol"
                ]
            ) = proto.upper()
            (
                req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"]["access_request"]["services"]["service"][0][
                    "port"
                ]
            ) = int(port)
            (
                req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"]["access_request"]["action"]
            ) = action.capitalize()
            req["ticket"]["comment"] = comment
            tos_request("sc", "POST", "/securechangeworkflow/api/securechange/tickets", data=json.dumps(req))
            entry["Contents"] = {"status": "Ticket Created"}
            entry["HumanReadable"] = tableToMarkdown(
                f"{req_type} ticket request", {"status": "Success"}, ["status"], removeNull=True
            )
            entry["EntryContext"]["Tufin.Request.Status"] = "Success"
            return entry
        elif req_type.lower() == "server decommission request":
            # Check for valid input
            if priority.capitalize() not in ["Critical", "High", "Normal", "Low"]:
                return_error("Priority must be Critical, High, Normal or Low")
            # Build change request JSON
            req = SERVER_DECOM_REQ
            req["ticket"]["subject"] = subj
            req["ticket"]["priority"] = priority.capitalize()
            (
                req["ticket"]["steps"]["step"][0]["tasks"]["task"]["fields"]["field"]["server_decommission_request"]["servers"][
                    "server"
                ]["ip_address"]
            ) = src
            req["ticket"]["comment"] = comment
            tos_request("st", "POST", "/securechangeworkflow/api/securechange/tickets", data=json.dumps(req))
            entry["Contents"] = {"status": "Ticket Created"}
            entry["HumanReadable"] = tableToMarkdown(
                f"{req_type} ticket request", {"status": "Success"}, ["status"], removeNull=True
            )
            entry["EntryContext"]["Tufin.Request.Status"] = "Success"
            return entry
    except Exception as e:
        return_error(f"Error submitting request: {str(e)}")
    return entry


def change_req_command():
    req_type = demisto.getArg("request-type")
    subject = demisto.getArg("subject")
    priority = demisto.getArg("priority")
    source = demisto.getArg("source")
    destination = demisto.getArg("destination")
    protocol = demisto.getArg("protocol")
    port = demisto.getArg("port")
    action = demisto.getArg("action")
    comment = demisto.getArg("comment")
    e = change_req(req_type, subject, priority, source, destination, protocol, port, action, comment)
    demisto.results(e)


def dev_search(name="", ip="", vendor="", model=""):
    """Search SecureTrack Devices"""
    entry: dict[str, Any] = {
        "Type": entryTypes["note"],
        "Contents": "",
        "ContentsFormat": formats["text"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "",
        "EntryContext": {},
    }
    dev_list = []
    try:
        qstr = "?show_os_version=true"
        if name != "" and name is not None:
            qstr = f"{qstr}&name={name}"
        if ip != "" and ip is not None:
            qstr = f"{qstr}&ip={ip}"
        if vendor != "" and vendor is not None:
            qstr = f"{qstr}&vendor={vendor}"
        if model != "" and model is not None:
            qstr = f"{qstr}&model={model}"
        url = f"/securetrack/api/devices{qstr}"
        devices = tos_request("st", "GET", url)
        if devices["devices"]["count"] > 0:
            for device in devices["devices"]["device"]:
                if "ip" in device:
                    dev_list.append(
                        {
                            "ID": int(device["id"]),
                            "Name": device["name"],
                            "IP": device["ip"],
                            "Vendor": device["vendor"],
                            "Model": device["model"],
                        }
                    )
                else:
                    dev_list.append(
                        {
                            "ID": int(device["id"]),
                            "Name": device["name"],
                            "IP": "0.0.0.0",
                            "Vendor": device["vendor"],
                            "Model": device["model"],
                        }
                    )
            entry["Contents"] = devices
            entry["EntryContext"] = {"Tufin.Device": dev_list}
            entry["HumanReadable"] = tableToMarkdown(
                "Device Search Results", dev_list, ["ID", "Name", "IP", "Vendor", "Model"], removeNull=True
            )
    except Exception as e:
        return_error(f"Error submitting request: {str(e)}")
    return entry


def dev_search_command():
    name = demisto.getArg("name")
    ip = demisto.getArg("ip")
    vendor = demisto.getArg("vendor")
    model = demisto.getArg("model")
    e = dev_search(name, ip, vendor, model)
    demisto.results(e)


def change_info(ticket_id):
    """Get the information from a change request"""
    entry: dict[str, Any] = {
        "Type": entryTypes["note"],
        "Contents": "",
        "ContentsFormat": formats["text"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "",
        "EntryContext": {},
    }
    try:
        url = f"/securechangeworkflow/api/securechange/tickets/{ticket_id}"
        ticket = tos_request("sc", "GET", url)
        cur_step = ""
        if type(ticket["ticket"]["current_step"]) is not str:
            cur_step = ticket["ticket"]["current_step"]["name"]
        else:
            cur_step = ""
        comments = ""
        if type(ticket["ticket"]["comments"]) is not str:
            for comment in ticket["ticket"]["comments"]["comment"]:
                comments = "{}\n{}: {}".format(comments, comment["created"], comment["content"])
        else:
            comments = ""
        entry["Contents"] = ticket["ticket"]
        chg = {}
        chg["ID"] = ticket["ticket"]["id"]
        chg["Subject"] = ticket["ticket"]["subject"]
        chg["Priority"] = ticket["ticket"]["priority"]
        chg["Status"] = ticket["ticket"]["status"]
        chg["CurrentStep"] = cur_step
        chg["Requester"] = ticket["ticket"]["requester"]
        chg["WorkflowID"] = ticket["ticket"]["workflow"]["id"]
        chg["WorkflowName"] = ticket["ticket"]["workflow"]["name"]
        entry["EntryContext"]["Tufin.Ticket"] = [chg]
        entry["HumanReadable"] = tableToMarkdown(
            f"Ticket ID {ticket_id}",
            {
                "ID": ticket["ticket"]["id"],
                "Subject": ticket["ticket"]["subject"],
                "Priority": ticket["ticket"]["priority"],
                "Status": ticket["ticket"]["status"],
                "CurrentStep": cur_step,
                "Requester": ticket["ticket"]["requester"],
                "Comments": comments,
            },
            ["ID", "Subject", "Priority", "Status", "CurrentStep", "Requester", "Comments"],
        )
    except Exception as e:
        return_error(f"Error submitting request: {str(e)}")
    return entry


def change_info_command():
    ticket_id = demisto.getArg("ticket-id")
    e = change_info(ticket_id)
    demisto.results(e)


def app_search(name=""):
    """Search for applications in SecureApp"""
    entry: dict[str, Any] = {
        "Type": entryTypes["note"],
        "Contents": "",
        "ContentsFormat": formats["text"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "",
        "EntryContext": {},
    }
    try:
        url = "/securechangeworkflow/api/secureapp/repository/applications"
        if name is not None and name != "":
            url = f"{url}?name={name}"
        apps = tos_request("sa", "GET", url)
        app_list = []
        for app in apps["applications"]["application"]:
            app_list.append(
                {
                    "ID": app["id"],
                    "Name": app["name"],
                    "Status": app["status"],
                    "Decommissioned": app["decommissioned"],
                    "OwnerID": app["owner"]["id"],
                    "OwnerName": app["owner"]["name"],
                    "Comments": app["comment"],
                }
            )
        entry["Contents"] = app_list
        entry["EntryContext"]["Tufin.App"] = app_list
        entry["HumanReadable"] = tableToMarkdown(
            "Application Search Results", app_list, ["ID", "Name", "Status", "OwnerName", "Comments"], removeNull=True
        )
    except Exception as e:
        return_error(f"Error submitting request: {str(e)}")
    return entry


def app_search_command():
    name = demisto.getArg("name")
    e = app_search(name)
    demisto.results(e)


def app_conns(app_id):
    """Get application connections from SecureApp"""
    entry: dict[str, Any] = {
        "Type": entryTypes["note"],
        "Contents": "",
        "ContentsFormat": formats["text"],
        "ReadableContentsFormat": formats["markdown"],
        "HumanReadable": "",
        "EntryContext": {},
    }
    try:
        url = f"/securechangeworkflow/api/secureapp/repository/applications/{app_id}/connections"
        conns = tos_request("sa", "GET", url)
        conn_list = []
        conn_md = f"### Connections for application ID: {app_id}"
        for conn in conns["connections"]["connection"]:
            conn_md = "{}\n#### Connection: {} ({})\nStatus: **{}**\nExternal: **{}**\nComment: **{}**".format(
                conn_md, conn["name"], conn["id"], conn["status"], conn["external"], conn["comment"]
            )
            # Get sources
            source_list = []
            conn_md = f"{conn_md}\n\n**Source:**\nid|type|name\n---|---|---"
            for source in conn["sources"]["source"]:
                source_list.append(source["name"])
                conn_md = "{}\n{} | {} | {}".format(conn_md, source["id"], source["type"], source["name"])
            # Get destinations
            dest_list = []
            conn_md = f"{conn_md}\n\n**Destination:**\nid|type|name\n---|---|---"
            for dest in conn["destinations"]["destination"]:
                dest_list.append(dest["name"])
                conn_md = "{}\n{} | {} | {}".format(conn_md, dest["id"], dest["type"], dest["name"])
            # Get services
            service_list = []
            conn_md = f"{conn_md}\n\n**Service:**\nid|name\n---|---"
            for service in conn["services"]["service"]:
                service_list.append(service["name"])
                conn_md = "{}\n{} | {}".format(conn_md, service["id"], service["name"])
            # Add to connection list
            conn_list.append(
                {
                    "ID": conn["id"],
                    "Name": conn["name"],
                    "AppID": app_id,
                    "Status": conn["status"],
                    "External": conn["external"],
                    "Source": source_list,
                    "Destination": dest_list,
                    "Service": service_list,
                    "Comment": conn["comment"],
                }
            )
        entry["Contents"] = conn_list
        entry["EntryContext"]["Tufin.AppConnection"] = conn_list
        entry["HumanReadable"] = conn_md
    except Exception as e:
        return_error(f"Error submitting request: {str(e)}")
    return entry


def app_conns_command():
    app_id = demisto.getArg("application-id")
    e = app_conns(app_id)
    demisto.results(e)


def test_command():
    tos_request("st", "GET", "/securetrack/api/devices")
    demisto.results("ok")


# Demisto Command Routing
try:
    if demisto.command() == "test-module":
        # This is the call made when pressing the integration test button.
        test_command()
    elif demisto.command() == "tufin-search-topology":
        path_finder_command()
    elif demisto.command() == "tufin-object-resolve":
        object_lookup_command()
    elif demisto.command() == "tufin-search-topology-image":
        path_finder_image_command()
    elif demisto.command() == "tufin-policy-search":
        policy_search_command()
    elif demisto.command() == "tufin-get-zone-for-ip":
        zone_match_command()
    elif demisto.command() == "tufin-submit-change-request":
        change_req_command()
    elif demisto.command() == "tufin-search-devices":
        dev_search_command()
    elif demisto.command() == "tufin-get-change-info":
        change_info_command()
    elif demisto.command() == "tufin-search-applications":
        app_search_command()
    elif demisto.command() == "tufin-search-application-connections":
        app_conns_command()
except Exception as e:
    return_error(e)