palo_alto_networks_pan_os_edl_management Deprecated

Deprecated. Use the Generic Export Indicators Service integration instead. This integration is still supported however, for customers with over 1000 Firewalls.

Utilities · Palo Alto Networks PAN-OS EDL Management (Deprecated)

Details

IDpalo_alto_networks_pan_os_edl_management
ProviderPalo Alto Networks
CategoryUtilities
From Version5.0.0
Docker Imagedemisto/openssh:1.0.0.12410
Supported ModulesAgentix

README

Deprecated. Use the Generic Export Indicators Service integration instead. This integration is still supported however, for customers with over 1000 Firewalls.
This integration requires root access in order to execute ssh commands.
If you’ve configured the server to run Docker images with a non-root internal user make sure to exclude the demisto/openssh Docker image as documented here.

Configure Palo Alto Networks PAN-OS EDL Management in Cortex

Parameter Required
Hostname or IP of server True
Server port False
SSH credentials to server (username and certificate, see in the credential manager) True
Password True
SSH extra parameters (e.g., “-c ChaCha20”) False
SCP extra parameters (e.g., “-c ChaCha20 -l 8000”) False
Document root (e.g., var/www/html/files) 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.

pan-os-edl-get-external-file


Displays the contents of the specified remote file located in the War Room.

Base Command

pan-os-edl-get-external-file

Input

Argument Name Description Required
file_path Unique path to the file on a remote server. Required
retries Number of retries. Default is 5. Optional

Context Output

There is no context output for this command.

Command Example

!pan-os-edl-get-external-file file_path=test_playbook_list4.txt

Human Readable Output

File Content

List
jojo.com
koko.com
upload.wikimedia.org

pan-os-edl-search-external-file


Searches for a string in a remote file.

Base Command

pan-os-edl-search-external-file

Input

Argument Name Description Required
file_path Unique path to the file on a remote server. Required
search_string String to search for in the remote file. Required

Context Output

There is no context output for this command.

Command Example

!pan-os-edl-search-external-file file_path=test_playbook_list4.txt search_string=koko

Human Readable Output

Search Results for koko

Result
 
koko.com

pan-os-edl-update


Updates the instance context with the specified list name and list items, and then overrides the path of the remote file with the internal list.

Base Command

pan-os-edl-update

Input

Argument Name Description Required
list_name List from the instance context with which to override the remote file. Required
file_path Unique path to file. Required
verbose Prints the updated remote file to the War Room. Default is “false”. Possible values are: true, false. Default is false. Optional
list_items List items. Required
add_or_remove Whether to add to, or remove from the list. Default is “add”. Possible values are: add, remove. Default is add. Required

Context Output

There is no context output for this command.

Command Example

!pan-os-edl-update add_or_remove=remove file_path=test_playbook_list4.txt list_items=toto.com list_name=test_playbook_list4 verbose=true

Human Readable Output

Updated File Data

Data
jojo.com
koko.com
upload.wikimedia.org

pan-os-edl-update-from-external-file


Updates internal list data with the contents of a remote file.

Base Command

pan-os-edl-update-from-external-file

Input

Argument Name Description Required
file_path Unique path to the file on a remote server. Required
list_name List name. Required
type Update type. “Merge” adds non-duplicate values, “Override” deletes existing data in the internal list. Default is “merge”. Possible values are: merge, override. Default is merge. Required
verbose Prints the updated internal list to the War Room. Default is “false”. Possible values are: true, false. Default is false. Optional
retries Number of retries. Default is 5. Optional

Context Output

There is no context output for this command.

Command Example

!pan-os-edl-update-from-external-file file_path=test_playbook_list4.txt list_name=test_playbook_list4 type=override

Human Readable Output

Instance context updated successfully

pan-os-edl-delete-external-file


Deletes a file from a remote server.

Base Command

pan-os-edl-delete-external-file

Input

Argument Name Description Required
file_path Unique path to the file on a remote server. Required

Context Output

There is no context output for this command.

Command Example

!pan-os-edl-delete-external-file file_path=test_playbook_list5.txt

Human Readable Output

File deleted successfully

pan-os-edl-print-internal-list


Displays internal list data in the War Room.

Base Command

pan-os-edl-print-internal-list

Input

Argument Name Description Required
list_name List name. Required

Context Output

There is no context output for this command.

Command Example

!pan-os-edl-print-internal-list list_name=test_playbook_list4

Human Readable Output

List items

test_playbook_list4
jojo.com
koko.com
upload.wikimedia.org

pan-os-edl-dump-internal-list


Dumps (copies) instance context to either the incident context or a file.

Base Command

pan-os-edl-dump-internal-list

Input

Argument Name Description Required
destination List data destination. Default is “file”. Possible values are: file, incident_context. Default is file. Required
list_name List name. Required

Context Output

Path Type Description
PANOSEDL.ListItems string Items of the internal list.
PANOSEDL.ListName string Name of the internal list.

Command Example

!pan-os-edl-dump-internal-list list_name=test_playbook_list4 destination=incident_context

Context Example

{
    "PANOSEDL": {
        "ListItems": [
            "jojo.com",
            "koko.com",
            "upload.wikimedia.org"
        ],
        "ListName": "test_playbook_list4"
    }
}

Human Readable Output

List items

test_playbook_list4
jojo.com
koko.com
upload.wikimedia.org

pan-os-edl-list-internal-lists


Displays instance context list names.

Base Command

pan-os-edl-list-internal-lists

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example


#### Human Readable Output

>### Instance context Lists
>
>|List names|
>|---|
>| test_playbook_list4 |

### pan-os-edl-search-internal-list

***
Search for a string in internal list.

#### Base Command

`pan-os-edl-search-internal-list`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| list_name | Name of list. | Required |
| search_string | String to search for in the remote file. | Required |

#### Context Output

There is no context output for this command.

#### Command Example

```!pan-os-edl-search-internal-list list_name=test_playbook_list4 search_string=koko.com```

#### Human Readable Output

>Search string koko.com is in the internal list test_playbook_list4.

### pan-os-edl-compare

***
Compares internal list and external file contents.

#### Base Command

`pan-os-edl-compare`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| list_name | List name. | Required |
| file_path | Unique path to the file on a remote server. | Required |
| retries | Number of retries. Default is 5. | Optional |

#### Context Output

There is no context output for this command.

#### Command Example

```!pan-os-edl-compare file_path=test_playbook_list4.txt list_name=list_name=test_playbook_list4```

#### Human Readable Output

>List was not found in instance context.

### pan-os-edl-get-external-file-metadata

***
Gets metadata for an external file.

#### Base Command

`pan-os-edl-get-external-file-metadata`

#### Input

| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| file_path | Unique path to the file on a remote server. | Required |

#### Context Output

| **Path** | **Type** | **Description** |
| --- | --- | --- |
| PANOSEDL.FileName | String | Name of the external file. |
| PANOSEDL.Size | Number | File size. |
| PANOSEDL.NumberOfLines | Number | Number of lines. |
| PANOSEDL.LastModified | String | Date that the file was last modified. |

#### Command Example

```!pan-os-edl-get-external-file-metadata file_path=test_playbook_list4.txt```

#### Context Example

```json
{
    "PANOSEDL": {
        "FileName": "test_playbook_list4.txt",
        "LastModified": "2021-07-11 06:46:21.290803188",
        "NumberOfLines": 3,
        "Size": 38
    }
}

Human Readable Output

File metadata

FileName Size NumberOfLines LastModified
test_playbook_list4.txt 38 3 2021-07-11 06:46:21.290803188

pan-os-edl-update-internal-list


Updates the instance context with the specified list name and list items.

Base Command

pan-os-edl-update-internal-list

Input

Argument Name Description Required
list_name The list from the instance context to update. Required
list_items An array of list items. Required
verbose Whether to print the updated remote file to the War Room. Can ve “true” or “false”. Default is “false”. Possible values are: true, false. Default is false. Optional
add_or_remove Whether to add to, or remove from the list. Can be “add” or “remove”. Default is “add”. Possible values are: add, remove. Default is add. Required

Context Output

There is no context output for this command.

Command Example

!pan-os-edl-update-internal-list add_or_remove=add list_items=toto.com list_name=test_playbook_list4

Human Readable Output

Instance context updated successfully.

pan-os-edl-update-external-file


Updates a remote file with the contents of an internal list.

Base Command

pan-os-edl-update-external-file

Input

Argument Name Description Required
file_path Unique path to the file on a remote server. Required
list_name List name. Required
verbose Whether to add to, or remove from the list. Can be “add” or “remove”. Default is “add”. Possible values are: true, false. Default is false. Optional

Context Output

There is no context output for this command.

Command Example

!pan-os-edl-update-external-file file_path=test_playbook_list4.txt list_name=test_playbook_list4 verbose=true

Human Readable Output

Updated File Data

Data
jojo.com
koko.com
toto.com
upload.wikimedia.org

Configuration parameters

  • hostname — Hostname or IP of server (required)
  • port — Server port
  • Authentication — SSH credentials to server (username and certificate, see in the credential manager) (required)
  • ssh_extra_params — SSH extra parameters (e.g., "-c ChaCha20")
  • scp_extra_params — SCP extra parameters (e.g., "-c ChaCha20 -l 8000")
  • document_root — Document root (e.g., var/www/html/files)

Commands (13)

  • pan-os-edl-compare

    Compares internal list and external file contents.

  • pan-os-edl-delete-external-file

    Deletes a file from a remote server.

  • pan-os-edl-dump-internal-list

    Dumps (copies) instance context to either the incident context or a file.

  • pan-os-edl-get-external-file

    Displays the contents of the specified remote file located in the War Room.

  • pan-os-edl-get-external-file-metadata

    Gets metadata for an external file.

  • pan-os-edl-list-internal-lists

    Displays instance context list names.

  • pan-os-edl-print-internal-list

    Displays internal list data in the War Room.

  • pan-os-edl-search-external-file

    Searches for a string in a remote file.

  • pan-os-edl-search-internal-list

    Search for a string in internal list.

  • pan-os-edl-update

    Updates the instance context with the specified list name and list items, and then overrides the path of the remote file with the internal list.

  • pan-os-edl-update-external-file

    Updates a remote file with the contents of an internal list.

  • pan-os-edl-update-from-external-file

    Updates internal list data with the contents of a remote file.

  • pan-os-edl-update-internal-list

    Updates the instance context with the specified list name and list items.

import shutil
import subprocess
import tempfile
from urllib.parse import urlparse, urlunparse

from CommonServerPython import *

''' GLOBALS '''

HOSTNAME = ''
USERNAME = ''
PORT = ''
SSH_EXTRA_PARAMS = ''
SCP_EXTRA_PARAMS = ''
DOCUMENT_ROOT = ''
CERTIFICATE_FILE = tempfile.NamedTemporaryFile(delete=False, mode='w')
INTEGRATION_COMMAND_NAME = 'pan-os-edl'


def initialize_instance(params: Dict[str, str]) -> None:
    global HOSTNAME, USERNAME, PORT, SSH_EXTRA_PARAMS, SCP_EXTRA_PARAMS, DOCUMENT_ROOT, CERTIFICATE_FILE

    authentication = params.get('Authentication', {})  # type: ignore

    HOSTNAME = str(params.get('hostname', ''))  # type: ignore
    USERNAME = str(authentication.get('identifier', ''))  # type: ignore
    PORT = str(params.get('port')) if params.get('port', '') and len(params.get('port')) > 0 else ''  # type: ignore

    SSH_EXTRA_PARAMS = params.get('ssh_extra_params').split() if params.get(  # type: ignore
        'ssh_extra_params') else None
    SCP_EXTRA_PARAMS = params.get('scp_extra_params').split() if params.get(  # type: ignore
        'scp_extra_params') else None
    DOCUMENT_ROOT = f'/{params.get("document_root")}' if params.get('document_root') else ''

    create_certificate_file(authentication)


def create_certificate_file(authentication) -> None:
    password = authentication.get('password', None)
    certificate = None
    if 'credentials' in authentication and 'sshkey' in authentication['credentials'] and len(
            authentication['credentials']['sshkey']) > 0:
        certificate = authentication.get('credentials', None).get('sshkey')

    if certificate:
        CERTIFICATE_FILE.write(certificate)
        CERTIFICATE_FILE.flush()
        os.chmod(CERTIFICATE_FILE.name, 0o400)
    elif password:
        # check that password field holds a certificate and not a password
        if password.find('-----') == -1:
            raise DemistoException('Password parameter must contain a certificate.')
        # split certificate by dashes
        password_list = password.split('-----')
        # replace spaces with newline characters
        password_fixed = '-----'.join(password_list[:2] + [password_list[2].replace(' ', '\n')] + password_list[3:])
        CERTIFICATE_FILE.write(password_fixed)
        CERTIFICATE_FILE.flush()
        os.chmod(CERTIFICATE_FILE.name, 0o400)
    else:
        raise DemistoException('To connect to the remote server, provide a certificate.')


''' UTILS '''


def ssh_execute(command: str):
    if PORT and SSH_EXTRA_PARAMS:
        param_list = ['ssh', '-o', 'StrictHostKeyChecking=no', '-i', CERTIFICATE_FILE.name, '-p',
                      PORT] + SSH_EXTRA_PARAMS + [USERNAME + '@' + HOSTNAME, command]  # type: ignore
        result = subprocess.run(param_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    elif PORT:
        result = subprocess.run(
            ['ssh', '-o', 'StrictHostKeyChecking=no', '-i', CERTIFICATE_FILE.name, '-p', PORT,
             USERNAME + '@' + HOSTNAME, command], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    elif SSH_EXTRA_PARAMS:
        param_list = ['ssh', '-o', 'StrictHostKeyChecking=no', '-i',
                      CERTIFICATE_FILE.name] + SSH_EXTRA_PARAMS + [USERNAME + '@' + HOSTNAME, command]  # type: ignore
        result = subprocess.run(param_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    else:
        result = subprocess.run(
            ['ssh', '-o', 'StrictHostKeyChecking=no', '-i', CERTIFICATE_FILE.name, USERNAME + '@' + HOSTNAME, command],
            stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

    if result.returncode != 0:
        if result.stderr:
            if result.stderr.find("Warning: Permanently added") != -1:
                return result.stdout  # ignore addition of new hosts warnings
            elif result.stderr.find("Permission denied") != -1:
                raise DemistoException(
                    'Permission denied, check your username and certificate.\n' + 'Got error: ' + result.stderr)
            else:
                raise DemistoException(result.stderr)
        elif command.find('grep') != -1 and result.returncode == 1:
            #  a search command that did not find any value
            demisto.results({
                'Type': 11,
                'Contents': 'Search string was not found in the external file path given.',
                'ContentsFormat': formats['text']
            })
            sys.exit(0)
        else:
            raise DemistoException(f'Command failed with exit status:{str(result.returncode)}')

    return result.stdout


def scp_execute(file_name: str, file_path: str):
    if SCP_EXTRA_PARAMS:
        param_list = ['scp', '-o', 'StrictHostKeyChecking=no',
                      '-i', CERTIFICATE_FILE.name] + SCP_EXTRA_PARAMS + [file_name,  # type: ignore
                                                                         f'{USERNAME}@{HOSTNAME}:\'{file_path}\'']
        result = subprocess.run(param_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
    else:
        param_list = ['scp', '-o', 'StrictHostKeyChecking=no', '-i', CERTIFICATE_FILE.name, file_name,
                      f'{USERNAME}@{HOSTNAME}:\'{file_path}\'']
        result = subprocess.run(param_list, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)

    if result.returncode != 0:
        if result.stderr:
            if result.stderr.find("Warning: Permanently added") != -1:
                return True  # ignore addition of new hosts warnings
            else:
                raise DemistoException(result.stderr)
        else:
            raise DemistoException(f'Command failed with exit status:{str(result.returncode)}')
    else:
        return True


def parse_url(item: str) -> str:
    """ Parse url if in url form to valid EDL form - without http / https

    Args:
        item(str): Item to parse.

    Returns:
        str: parsed item, if URL returned without http / https

    Examples:
        >>> parse_url('http://google.com')
        'google.com'
        >>> parse_url('https://google.com')
        'google.com'
        >>> parse_url('https://google.com/hello_world')
        'google.com/hello_world'
        >>> parse_url('not url')
        'not url'
    """
    try:
        url_obj = urlparse(item)._replace(scheme='')
        return urlunparse(url_obj).replace('//', '')
    except ValueError:
        return item


def parse_items(items: str) -> List[str]:
    """ Parse list of item to update, parsing steps:
        1. Remove http and https from

    Args:
        items(str): items for update

    Returns:
        list: list of parsed items.
    """
    return [parse_url(item) for item in argToList(items)]


''' COMMANDS '''


def edl_get_external_file(file_path: str, retries: int = 1) -> str:
    command = f'cat \'{file_path}\''
    while retries > 0:
        result = ssh_execute(command)
        # counting newlines as in some edge cases the external web server returns the file content intermittently
        # with newline as every other char
        num_lines = float(result.count('\n'))
        if num_lines > len(result) / 3:
            demisto.info(f'The number of newlines chars in the file is too big. Try number {retries} before failure.')
            retries -= 1
        else:
            return result

    # if we get here, we failed as the file contains too many newlines to be valid
    raise DemistoException('The file contains too many newlines to be valid. '
                           'Please check the file contents on the external web server manually.')


def edl_get_external_file_command(args: dict):
    """
    Get external file from web-server and prints to the war room
    """
    file_path = str(args.get('file_path', ''))
    retries = int(args.get('retries', '1'))

    if DOCUMENT_ROOT:
        file_path = os.path.join(DOCUMENT_ROOT, file_path)

    result = edl_get_external_file(file_path, retries)
    sorted_list = sorted(result.split('\n'))

    md = tableToMarkdown('File Content:', sorted_list, headers=['List'])
    demisto.results({
        'ContentsFormat': formats['markdown'],
        'Type': entryTypes['note'],
        'Contents': md
    })


def edl_search_external_file(file_path: str, search_string: str):
    return ssh_execute(f'grep \'{search_string}\' \'{file_path}\'')


def edl_search_external_file_command(args: dict):
    """
    Search the external file and return all matching entries to Warroom
    """
    file_path: str = str(args.get('file_path', ''))
    if DOCUMENT_ROOT:
        file_path = os.path.join(DOCUMENT_ROOT, file_path)
    search_string: str = str(args.get('search_string', ''))

    result = edl_search_external_file(file_path, search_string)
    sorted_list = sorted(result.split('\n'))

    md = tableToMarkdown(f'Search Results for {search_string}:', sorted_list, headers=['Result'])

    demisto.results({
        'ContentsFormat': formats['markdown'],
        'Type': entryTypes['note'],
        'Contents': md
    })


def edl_update_external_file(file_path: str, list_name: str, verbose: bool):
    dict_of_lists = demisto.getIntegrationContext()
    list_data = sorted(dict_of_lists.get(list_name))

    file_name = file_path.rsplit('/', 1)[-1]
    if not file_name.endswith('.txt'):
        file_name += '.txt'

    try:
        with open(file_name, 'w') as file:
            file.write("\n".join(list_data))
        success = scp_execute(file_name, file_path)
    finally:
        shutil.rmtree(file_name, ignore_errors=True)

    if not success:
        raise DemistoException('External file was not updated successfully.')
    else:
        if verbose:
            external_file_items = ssh_execute(f'cat \'{file_path}\'')
            if external_file_items:
                md = tableToMarkdown('Updated File Data:', external_file_items, headers=['Data'])
            else:
                md = 'External file has no items.'
        else:
            md = 'External file updated successfully.'

        demisto.results({
            'Type': entryTypes['note'],
            'Contents': md,
            'ContentsFormat': formats['markdown']
        })


def edl_update_external_file_command(args: dict):
    """
    Overrides external file path with internal list
    """
    file_path: str = str(args.get('file_path', ''))
    if DOCUMENT_ROOT:
        file_path = os.path.join(DOCUMENT_ROOT, file_path)
    list_name: str = str(args.get('list_name', ''))
    verbose = args.get('verbose') == 'true'

    edl_update_external_file(file_path, list_name, verbose)


def edl_update_internal_list(list_name: str, list_items: list, add: bool, verbose: bool):
    dict_of_lists = demisto.getIntegrationContext()

    if not dict_of_lists:
        demisto.debug('PAN-OS EDL Management integration context is empty.')
        dict_of_lists = {list_name: list_items}
        if verbose:
            md = tableToMarkdown('List items:', list_items, headers=[list_name])
        else:
            md = 'Instance context updated successfully.'
    else:
        if not dict_of_lists.get(list_name, None) and not add:
            raise Exception(f'Cannot remove items from an empty list: {list_name}.')

        if dict_of_lists.get(list_name, None):
            if add:
                chosen_list = dict_of_lists.get(list_name)
                if not isinstance(chosen_list, list):
                    chosen_list = [chosen_list]

                list_items = list(set(chosen_list + list_items))
            else:  # remove
                list_items = [item for item in dict_of_lists.get(list_name) if item not in list_items]

        if not add and len(list_items) == 0:
            # delete list from instance context, can happen only upon remove of objects
            demisto.debug(f'PAN-OS EDL Management deleting {list_name} from the integration context.')
            dict_of_lists.pop(list_name, None)
            md = 'List is empty, deleted from instance context.'
        else:
            # update list in instance context, can happen upon removal or addition of objects
            sorted_list = sorted(list_items)
            dict_of_lists.update({list_name: sorted_list})
            if verbose:
                md = tableToMarkdown('List items:', sorted_list, headers=[list_name])
            else:
                md = 'Instance context updated successfully.'

    if not dict_of_lists:  # to be removed, debugging purposes only
        demisto.debug('PAN-OS EDL Management updating an empty object to the integration context.')

    demisto.debug(f'PAN-OS EDL Management updating {list_name} with {len(list_items)} in the integration context.')
    demisto.setIntegrationContext(dict_of_lists)

    demisto.results({
        'ContentsFormat': formats['markdown'],
        'Type': entryTypes['note'],
        'Contents': md
    })


def edl_update_internal_list_command(args: dict):
    """
        Updates the instance context with the list name and items given
    """
    list_name: str = str(args.get('list_name', ''))
    list_items: list = argToList(str(args.get('list_items', '')))
    if args.get('add_or_remove') not in ['add', 'remove']:
        raise Exception('add_or_remove argument is not \'add\' neither \'remove\'.')
    add = args.get('add_or_remove') == 'add'
    verbose = args.get('verbose') == 'true'

    edl_update_internal_list(list_name, list_items, add, verbose)


def edl_update(args: dict):
    """
    Updates the instance context with the list name and items given
    Overrides external file path with internal list
    """
    file_path: str = str(args.get('file_path', ''))
    if DOCUMENT_ROOT:
        file_path = os.path.join(DOCUMENT_ROOT, file_path)

    # Parse list items
    list_name: str = str(args.get('list_name', ''))
    list_items = parse_items(items=str(args.get('list_items', '')))
    if args.get('add_or_remove') not in ['add', 'remove']:
        raise DemistoException('add_or_remove argument is neither \'add\' nor \'remove\'.')
    add = args.get('add_or_remove') == 'add'
    verbose = args.get('verbose') == 'true'

    # update internal list
    edl_update_internal_list(list_name, list_items, add, verbose)

    # scp internal list to file_path
    edl_update_external_file(file_path, list_name, verbose)


def edl_update_from_external_file(list_name: str, file_path: str, type_: str, retries: int):
    dict_of_lists = demisto.getIntegrationContext()
    list_data = dict_of_lists.get(list_name, None)
    file_data = edl_get_external_file(file_path, retries)
    sorted_file_data = sorted(file_data.split('\n'))

    if list_data:
        set_internal = set(list_data)
        set_external = set(sorted_file_data)
        set_external.discard('')
        if type_ == 'merge':
            unified = set_internal.union(set_external)
            list_data_new = list(unified)
        else:  # type_ == 'override'
            list_data_new = list(set_external)
        sorted_list_data_new = sorted(list_data_new)
        dict_of_lists.update({list_name: sorted_list_data_new})
        demisto.setIntegrationContext(dict_of_lists)
        return sorted_list_data_new
    else:
        dict_of_lists.update({list_name: sorted_file_data})
        demisto.setIntegrationContext(dict_of_lists)
        return sorted_file_data


def edl_update_from_external_file_command(args: dict):
    """
    Updates internal list data with external file contents
    """
    file_path: str = str(args.get('file_path', ''))
    if DOCUMENT_ROOT:
        file_path = os.path.join(DOCUMENT_ROOT, file_path)
    list_name: str = str(args.get('list_name', ''))
    type_: str = args.get('type', 'false')
    verbose: bool = args.get('verbose') == 'true'
    retries: int = int(args.get('retries', '1'))

    list_data_new = edl_update_from_external_file(list_name, file_path, type_, retries)

    if verbose:
        md = tableToMarkdown('List items:', list_data_new, headers=[list_name])
    else:
        md = 'Instance context updated successfully'

    demisto.results({
        'Type': entryTypes['note'],
        'Contents': md,
        'ContentsFormat': formats['markdown']
    })


def edl_delete_external_file(file_path: str) -> str:
    ssh_execute(f'rm -f \'{file_path}\'')
    return 'File deleted successfully'


def edl_delete_external_file_command(args: dict):
    """
    Delete external file
    """
    file_path = str(args.get('file_path', ''))
    if DOCUMENT_ROOT:
        file_path = os.path.join(DOCUMENT_ROOT, file_path)
    result = edl_delete_external_file(file_path)

    demisto.results({
        'Type': entryTypes['note'],
        'Contents': result,
        'ContentsFormat': formats['text']
    })


def edl_list_internal_lists_command():
    """
    List all instance context lists
    """
    dict_of_lists = demisto.getIntegrationContext()
    list_names = sorted(list(dict_of_lists.keys()))

    md = tableToMarkdown('Instance context Lists:', list_names, headers=['List names'])

    demisto.results({
        'ContentsFormat': formats['markdown'],
        'Type': entryTypes['note'],
        'Contents': md
    })


def edl_search_internal_list_command(args: dict):
    """
    Search a string on internal list
    """
    list_name = args.get('list_name')
    search_string = args.get('search_string')

    dict_of_lists = demisto.getIntegrationContext()
    list_data = dict_of_lists.get(list_name, None)

    if not list_data:
        demisto.results({
            'Type': 11,
            'Contents': f'List {list_name} was not found in the instance context.',
            'ContentsFormat': formats['text']
        })
    elif search_string in list_data:
        demisto.results({
            'Type': entryTypes['note'],
            'Contents': f'Search string {search_string} is in the internal list {list_name}.',
            'ContentsFormat': formats['text']
        })
    else:
        demisto.results({
            'Type': 11,
            'Contents': f'Search string {search_string} was not found in the instance context list {list_name}.',
            'ContentsFormat': formats['text']
        })


def edl_print_internal_list_command(args: dict):
    """
    Print to the war room instance context list
    """
    list_name = str(args.get('list_name', ''))
    dict_of_lists = demisto.getIntegrationContext()
    list_data = sorted(dict_of_lists.get(list_name, None))

    if not list_data:
        demisto.results({
            'Type': 11,
            'Contents': 'List was not found in instance context.',
            'ContentsFormat': formats['text']
        })
    else:
        md = tableToMarkdown('List items:', list_data, headers=[list_name])
        demisto.results({
            'Type': entryTypes['note'],
            'Contents': md,
            'ContentsFormat': formats['markdown']
        })


def edl_dump_internal_list_command(args: dict):
    """
    Dumps an instance context list to either a file or incident context
    """
    destination = args.get('destination')
    list_name = str(args.get('list_name', ''))

    dict_of_lists = demisto.getIntegrationContext()
    list_data = sorted(dict_of_lists.get(list_name, []))
    if not list_data:
        demisto.results({
            'Type': 11,
            'Contents': 'List was not found in instance context or has no data.',
            'ContentsFormat': formats['text']
        })
        sys.exit(0)
    if destination == 'file':  # dump list as file
        internal_file_path = demisto.uniqueFile()

        try:
            with open(internal_file_path, 'w') as f:
                f.write("\n".join(list_data))
            file_type = entryTypes['entryInfoFile']
            with open(internal_file_path, 'rb') as file:
                file_entry = fileResult(internal_file_path, file.read(), file_type)
            demisto.results(file_entry)
        finally:
            shutil.rmtree(internal_file_path, ignore_errors=True)

    else:  # update incident context
        md = tableToMarkdown('List items:', list_data, headers=[list_name])
        ec = {
            'ListName': list_name,
            'ListItems': list_data
        }

        demisto.results({
            'Type': entryTypes['note'],
            'Contents': md,
            'ContentsFormat': formats['markdown'],
            'EntryContext': {
                "PANOSEDL(val.ListName == obj.ListName)": ec
            }
        })


def edl_compare_command(args: dict):
    list_name = str(args.get('list_name', ''))
    file_path = str(args.get('file_path', ''))
    retries = int(args.get('retries', '1'))

    if DOCUMENT_ROOT:
        file_path = os.path.join(DOCUMENT_ROOT, file_path)

    dict_of_lists = demisto.getIntegrationContext()
    list_data = dict_of_lists.get(list_name, None)
    if not list_data:
        demisto.results({
            'Type': 11,
            'Contents': 'List was not found in instance context.',
            'ContentsFormat': formats['text']
        })
        sys.exit(0)

    file_data = edl_get_external_file(file_path, retries)
    if not file_data:
        demisto.results({
            'Type': 11,
            'Contents': 'file was not found in external web-server.',
            'ContentsFormat': formats['text']
        })
        sys.exit(0)

    set_internal = set(list_data)
    set_external = set(file_data.split('\n'))
    set_external.discard('')

    unique_internal = set_internal - set_external
    unique_external = set_external - set_internal

    md = ''
    if unique_external:
        md += '### Warning: External file contains values that are not in the internal Demisto list.\n'
        md += '#### If these changes are unexpected, check who has permission to write to the external file.\n'
        md += tableToMarkdown('', list(unique_external),
                              headers=[file_path.rsplit('/')[-1]])
    if unique_internal:
        md += '### Warning: Internal list contains values that are not in the external file.\n'
        md += '#### If these changes are unexpected, check who has permission to write to the external file.\n'
        md += tableToMarkdown('', list(unique_internal), headers=[list_name])
    if len(md) == 0:
        md = 'Internal list and external file have the same values.'

    demisto.results({
        'Type': 11 if unique_external or unique_internal else entryTypes['note'],
        'Contents': md,
        'ContentsFormat': formats['markdown'],
    })


def edl_get_external_file_metadata_command(args: dict):
    file_path = str(args.get('file_path', ''))
    if DOCUMENT_ROOT:
        file_path = os.path.join(DOCUMENT_ROOT, file_path)

    result = ssh_execute(f'stat \'{file_path}\'')

    file_size = int(result.split("Size: ", 1)[1].split(" ", 1)[0])
    file_name = file_path.split("/")[-1]
    if len(file_name) < 0:
        file_name = file_path
    last_modified_parts = result.split("Change: ", 1)[1].split(" ", 2)[0:2]
    last_modified = ' '.join(last_modified_parts)

    number_of_lines = int(ssh_execute(f'wc -l < \'{file_path}\'')) + 1

    metadata_outputs = {
        'FileName': file_name,
        'Size': file_size,
        'LastModified': last_modified,
        'NumberOfLines': number_of_lines
    }

    demisto.results({
        'Type': entryTypes['note'],
        'Contents': result,
        'ContentsFormat': formats['text'],
        'ReadableContentsFormat': formats['markdown'],
        'HumanReadable': tableToMarkdown('File metadata:', metadata_outputs,
                                         ['FileName', 'Size', 'NumberOfLines', 'LastModified'], removeNull=True),
        'EntryContext': {"PANOSEDL(val.FileName == obj.FileName)": metadata_outputs}
    })


''' EXECUTION '''


def main() -> None:
    command = demisto.command()
    args = demisto.args()
    params = demisto.params()
    LOG(f'command is {command}')
    commands = {
        f'{INTEGRATION_COMMAND_NAME}-get-external-file': edl_get_external_file_command,
        f'{INTEGRATION_COMMAND_NAME}-search-external-file': edl_search_external_file_command,
        f'{INTEGRATION_COMMAND_NAME}-update-internal-list': edl_update_internal_list_command,
        f'{INTEGRATION_COMMAND_NAME}-update-external-file': edl_update_external_file_command,
        f'{INTEGRATION_COMMAND_NAME}-update': edl_update,
        f'{INTEGRATION_COMMAND_NAME}-update-from-external-file': edl_update_from_external_file_command,
        f'{INTEGRATION_COMMAND_NAME}-delete-external-file': edl_delete_external_file_command,
        f'{INTEGRATION_COMMAND_NAME}-search-internal-list': edl_search_internal_list_command,
        f'{INTEGRATION_COMMAND_NAME}-print-internal-list': edl_print_internal_list_command,
        f'{INTEGRATION_COMMAND_NAME}-dump-internal-list': edl_dump_internal_list_command,
        f'{INTEGRATION_COMMAND_NAME}-compare': edl_compare_command,
        f'{INTEGRATION_COMMAND_NAME}-get-external-file-metadata': edl_get_external_file_metadata_command,
    }
    try:
        initialize_instance(params=params)
        if command == 'test-module':
            ssh_execute('echo 1')
            return_results('ok')

        elif command == 'pan-os-edl-list-internal-lists':
            edl_list_internal_lists_command()

        elif command in commands:
            commands[command](args)
        else:
            raise NotImplementedError(f'Command "{command}" is not implemented in {INTEGRATION_COMMAND_NAME}.')

    except Exception as err:
        if str(err).find('warning') != -1:
            LOG(str(err))
        else:
            demisto.error(traceback.format_exc())  # print the traceback
            return_error(str(err), err)

    finally:
        shutil.rmtree(CERTIFICATE_FILE.name, ignore_errors=True)
        LOG.print_log()


if __name__ in ('__main__', '__builtin__', 'builtins'):
    main()