GravityZone

GravityZone provides secure access to incident and endpoint data and enables remediation actions through its APIs.

Endpoint · GravityZone

Details

IDGravityZone
CategoryEndpoint
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10381435

README

GravityZone provides secure access to incident and endpoint data and enables remediation actions through its APIs.
This integration was integrated and tested with version 6.6 of GravityZone.

Configure GravityZone in Cortex

Parameter Description Required
Server URL The URL of your GravityZone Cloud instance. True
API key API key to access the service REST API. True
Trust any certificate (not secure) When selected, the server certificates are not verified. False
Use system proxy settings Use the system proxy settings for connecting to the server. False
Fetch incidents When selected, the integration will fetch incidents from the server. False
Maximum incidents to fetch Maximum number of incidents per fetch. The default value is 50. False
First fetch time The time period from which the first fetch will start. False
Mirroring Direction The mirroring direction in which to mirror the incident. You can mirror “Incoming” (from GravityZone to Cortex XSOAR), “Outgoing” (from Cortex XSOAR to GravityZone), or in both directions. False
Incident type   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.

fetch-incidents


Retrieves incidents from GravityZone Cloud.

Base Command

fetch-incidents

Context Output

There is no context output for this command.

gz-endpoint-list


Retrieves the list of managed endpoints.

Base Command

gz-endpoint-list

Input

Argument Name Description Required
limit The maximum number of endpoints to retrieve. Optional
name The endpoint name filter. Returns endpoints whose hostnames start with the specified value. The value must be at least 3 characters long. Supports an optional single leading ‘*’ wildcard pattern. Optional
node_id The ID of the target GravityZone company or network inventory group. If not specified, the default value is the ID of the company associated with the API key used to make the request. Optional

Context Output

Path Type Description
GravityZone.EndpointsList.ID String The endpoint ID.
GravityZone.EndpointsList.Hostname String The endpoint hostname.
GravityZone.EndpointsList.IP String The endpoint IP address.
GravityZone.EndpointsList.OS String The endpoint operating system version.
GravityZone.EndpointsList.MAC String The endpoint MAC address.
GravityZone.EndpointsList.Vendor String The endpoint vendor.
GravityZone.EndpointsList.FQDN String The endpoint’s fully qualified domain name (FQDN).
GravityZone.EndpointsList.CompanyID String The GravityZone company ID where the endpoint is located.
GravityZone.EndpointsList.GroupID String The GravityZone group ID where the endpoint is located.

Command example


#### Context Example

```json
{
    "GravityZone": {
        "EndpointsList": [
            {
                "CompanyID": "6909d1859866985b410b2969",
                "FQDN": "win",
                "GroupID": "6909d1869866985b410b296e",
                "Hostname": "WIN",
                "ID": "699d81cdab613e65696e452e",
                "IP": "IP1",
                "MAC": "005056b1b175",
                "OS": "Windows 10 Pro",
                "Vendor": "GravityZone"
            },
            {
                "CompanyID": "6909d1859866985b410b2969",
                "FQDN": "xdr-nsva",
                "GroupID": "6909d1869866985b410b296e",
                "Hostname": "xdr-nsva",
                "ID": "69fc10a547f2e985e6774a0f",
                "IP": "IP2",
                "MAC": "005056b156b1",
                "OS": "Linux Ubuntu 24.04.4 LTS",
                "Vendor": "GravityZone"
            },
            {
                "CompanyID": "6909d1859866985b410b2969",
                "FQDN": "ta349cf3rjt5",
                "GroupID": "6909d1869866985b410b296e",
                "Hostname": "TA349CF3RJT5",
                "ID": "6a6043865de62c6f81270b8d",
                "IP": "IP3",
                "MAC": "005056b8bfd3",
                "OS": "Windows 10 Enterprise",
                "Vendor": "GravityZone"
            }
        ]
    }
}

Human Readable Output

GravityZone Endpoints List

ID Hostname IP OS MAC Vendor FQDN CompanyID GroupID
699d81cdab613e65696e452e WIN IP1 Windows 10 Pro 005056b1b175 GravityZone win 6909d1859866985b410b2969 6909d1869866985b410b296e
69fc10a547f2e985e6774a0f xdr-nsva IP2 Linux Ubuntu 24.04.4 LTS 005056b156b1 GravityZone xdr-nsva 6909d1859866985b410b2969 6909d1869866985b410b296e
6a6043865de62c6f81270b8d TA349CF3RJT5 IP3 Windows 10 Enterprise 005056b8bfd3 GravityZone ta349cf3rjt5 6909d1859866985b410b2969 6909d1869866985b410b296e

Command example

!gz-endpoint-list name=TA66

Human Readable Output

GravityZone Endpoints List

No entries.

Command example

!gz-endpoint-list node_id=6909d1859866985b410b2969

Context Example

{
    "GravityZone": {
        "EndpointsList": [
            {
                "CompanyID": "6909d1859866985b410b2969",
                "FQDN": "win",
                "GroupID": "6909d1869866985b410b296e",
                "Hostname": "WIN",
                "ID": "699d81cdab613e65696e452e",
                "IP": "IP1",
                "MAC": "005056b1b175",
                "OS": "Windows 10 Pro",
                "Vendor": "GravityZone"
            },
            {
                "CompanyID": "6909d1859866985b410b2969",
                "FQDN": "xdr-nsva",
                "GroupID": "6909d1869866985b410b296e",
                "Hostname": "xdr-nsva",
                "ID": "69fc10a547f2e985e6774a0f",
                "IP": "IP2",
                "MAC": "005056b156b1",
                "OS": "Linux Ubuntu 24.04.4 LTS",
                "Vendor": "GravityZone"
            },
            {
                "CompanyID": "6909d1859866985b410b2969",
                "FQDN": "ta349cf3rjt5",
                "GroupID": "6909d1869866985b410b296e",
                "Hostname": "TA349CF3RJT5",
                "ID": "6a6043865de62c6f81270b8d",
                "IP": "IP3",
                "MAC": "005056b8bfd3",
                "OS": "Windows 10 Enterprise",
                "Vendor": "GravityZone"
            }
        ]
    }
}

Human Readable Output

GravityZone Endpoints List

ID Hostname IP OS MAC Vendor FQDN CompanyID GroupID
699d81cdab613e65696e452e WIN IP1 Windows 10 Pro 005056b1b175 GravityZone win 6909d1859866985b410b2969 6909d1869866985b410b296e
69fc10a547f2e985e6774a0f xdr-nsva IP2 Linux Ubuntu 24.04.4 LTS 005056b156b1 GravityZone xdr-nsva 6909d1859866985b410b2969 6909d1869866985b410b296e
6a6043865de62c6f81270b8d TA349CF3RJT5 IP3 Windows 10 Enterprise 005056b8bfd3 GravityZone ta349cf3rjt5 6909d1859866985b410b2969 6909d1869866985b410b296e

gz-endpoint-get


Retrieves endpoint details by endpoint ID.

Base Command

gz-endpoint-get

Input

Argument Name Description Required
id The ID of the endpoint to retrieve. Required

Context Output

Path Type Description
GravityZone.Endpoint.ID String The endpoint ID.
GravityZone.Endpoint.Hostname String The endpoint hostname.
GravityZone.Endpoint.IP String The endpoint IP address.
GravityZone.Endpoint.OS String The endpoint operating system version.
GravityZone.Endpoint.Status String The endpoint status.
GravityZone.Endpoint.Vendor String The endpoint vendor.
GravityZone.Endpoint.LastLoggedUsers String The last users who logged on the endpoint.
Endpoint.ID String The endpoint ID.
Endpoint.Hostname String The endpoint hostname.
Endpoint.IPAddress String The endpoint IP address.
Endpoint.OS String The endpoint operating system version.
Endpoint.Status String The endpoint status.
Endpoint.Vendor String The endpoint vendor.

Command example

!gz-endpoint-get id=6942a43afe8d4e463ca5c197

Context Example

{
    "Endpoint": {
        "Hostname": "bdvm",
        "ID": "6942a43afe8d4e463ca5c197",
        "IPAddress": "1.1.1.1",
        "OS": "Linux Ubuntu 24.04.2 LTS",
        "Status": "Online",
        "Vendor": "GravityZone"
    },
    "GravityZone": {
        "Endpoint": {
            "Hostname": "bdvm",
            "ID": "6942a43afe8d4e463ca5c197",
            "IP": "1.1.1.1",
            "LastLoggedUsers": "",
            "OS": "Linux Ubuntu 24.04.2 LTS",
            "Status": "Online",
            "Vendor": "GravityZone"
        }
    }
}

Human Readable Output

GravityZone Endpoint

ID Hostname IP OS Status Vendor LastLoggedUsers
6942a43afe8d4e463ca5c197 bdvm 1.1.1.1 Linux Ubuntu 24.04.2 LTS Online GravityZone  

gz-endpoint-download-investigation-package


Collects and downloads an investigation package from an endpoint.

Base Command

gz-endpoint-download-investigation-package

Input

Argument Name Description Required
id The ID of the endpoint from which to collect the investigation package. Required
output_file The output file name in Cortex XSOAR to save the collected package. For Windows and macOS use .zip, for Linux use .tgz. Required

Context Output

Path Type Description
GravityZone.Command.DownloadFile.EndpointID String The endpoint ID.
GravityZone.Command.DownloadFile.Status String The command status.
GravityZone.Command.DownloadFile.ErrorCode String The command error code.
GravityZone.Command.DownloadFile.OutputFile String The output file name.
GravityZone.Command.DownloadFile.RemoteFile String The remote file path.
GravityZone.Command.DownloadFile.FileID String The file ID.
File.SHA1 String The SHA1 hash of the downloaded file.
File.SHA256 String The SHA256 hash of the downloaded file.
File.SHA512 String The SHA512 hash of the downloaded file.
File.SSDeep String The SSDeep hash of the downloaded file.
File.EntryID String The EntryID hash of the downloaded file.
File.Info String Information about the downloaded file.
File.Type String The downloaded file type.
File.MD5 String The MD5 hash of the downloaded file.
File.Extension String The downloaded file extension.
File.Size Number The downloaded file size.
File.Name String The downloaded file name.

Command example

!gz-endpoint-download-investigation-package id=6942a43afe8d4e463ca5c197 output_file=investigation_package.zip

Context Example

{
    "GravityZone": {
        "Command": {
            "DownloadFile": {
                "ActivityID": "69443b020126750c1a0dbe2f",
                "ActivityType": "DownloadFile",
                "Status": "Success",
                "EndpointID": "6942a43afe8d4e463ca5c197",
                "ErrorCode": null,
                "RemoteFile": null,
                "OutputFile": "investigation_package.zip",
                "FileID": "LtvQ6B8eCFvTNo7bf3vDUD@25729aa7-7442-4231-8b98-ecb0fc29a642",
                "FileName": "investigation_package.zip"
            }
        }
    }
}

Human Readable Output

GravityZone.Command.DownloadFile command on host 6942a43afe8d4e463ca5c197

EndpointID Status ErrorCode OutputFile RemoteFile FileID
6942a43afe8d4e463ca5c197 Success   investigation_package.zip   LtvQ6B8eCFvTNo7bf3vDUD@25729aa7-7442-4231-8b98-ecb0fc29a642

gz-endpoint-download-file


Downloads a file from an endpoint.

Base Command

gz-endpoint-download-file

Input

Argument Name Description Required
id The ID of the endpoint to download the file from. Required
remote_file The full path of the remote file on the endpoint to download. Required
output_file The output file name in Cortex XSOAR to save the downloaded file to. Required

Context Output

Path Type Description
GravityZone.Command.DownloadFile.EndpointID String The endpoint ID.
GravityZone.Command.DownloadFile.Status String The command status.
GravityZone.Command.DownloadFile.ErrorCode String The command error code.
GravityZone.Command.DownloadFile.OutputFile String The output file name.
GravityZone.Command.DownloadFile.RemoteFile String The remote file path.
GravityZone.Command.DownloadFile.FileID String The file ID.
File.SHA1 String The SHA1 hash of the downloaded file.
File.SHA256 String The SHA256 hash of the downloaded file.
File.SHA512 String The SHA512 hash of the downloaded file.
File.SSDeep String The SSDeep hash of the downloaded file.
File.EntryID String The EntryID hash of the downloaded file.
File.Info String Information about the downloaded file.
File.Type String The downloaded file type.
File.MD5 String The MD5 hash of the downloaded file.
File.Extension String The downloaded file extension.
File.Size Number The downloaded file size.
File.Name String The downloaded file name.

Command example

!gz-endpoint-download-file id=6942a43afe8d4e463ca5c197 remote_file=/root/test/test.txt output_file=downloaded_file.gzip

Context Example

{
    "GravityZone": {
        "Command": {
            "DownloadFile": {
                "ActivityID": "6943fb82c1f339e18b0949a1",
                "ActivityType": "DownloadFile",
                "RemoteFile": "/root/test/test.txt",
                "Status": "Success",
                "EndpointID": "6942a43afe8d4e463ca5c197",
                "ErrorCode": null,
                "OutputFile": "downloaded_file.gzip",
                "FileID": "LtvQ6B8eCFvTNo7bf3vDUD@25729aa7-7442-4231-8b98-ecb0fc29a642",
                "FileName": "downloaded_file.gzip"
            }
        }
    }
}

Human Readable Output

GravityZone.Command.DownloadFile command on host 6942a43afe8d4e463ca5c197

EndpointID Status ErrorCode OutputFile RemoteFile FileID
6942a43afe8d4e463ca5c197 Success   downloaded_file.gzip /root/test/test.txt LtvQ6B8eCFvTNo7bf3vDUD@25729aa7-7442-4231-8b98-ecb0fc29a642

gz-endpoint-isolate


Isolates an endpoint from the network.

Base Command

gz-endpoint-isolate

Input

Argument Name Description Required
id The ID of the endpoint to isolate. Required

Context Output

Path Type Description
GravityZone.Command.Isolate.TaskID String The task ID.
GravityZone.Command.Isolate.TaskType String The task type.
GravityZone.Command.Isolate.EndpointID String The endpoint ID.
GravityZone.Command.Isolate.Hostname String The endpoint name.
GravityZone.Command.Isolate.Status String The command status.
GravityZone.Command.Isolate.ErrorCode String The command error code, or “Success”.
GravityZone.Command.Isolate.Error String The command error message, or “Success”.
GravityZone.Command.Isolate.StartDate Date The start date of the command execution.
GravityZone.Command.Isolate.EndDate Date The end date of the command execution.

Command example

!gz-endpoint-isolate id=6942a43afe8d4e463ca5c197

Context Example

{
    "GravityZone": {
        "Command" : {
            "Isolate" : {
                "TaskID": "6941bbc98ba450a5c10e5a16",
                "TaskType": "Isolate",
                "Status": "Processed",
                "EndDate": "2025-12-16T22:06:33Z",
                "EndpointID": "6942a43afe8d4e463ca5c197",
                "Hostname": "ENDPOINT_NAME",
                "ErrorCode": "Success",
                "Error": "Success",
                "StartDate": "2025-12-16T22:06:33Z"
            }
        }
    }
}

Human Readable Output

GravityZone.Command.Isolate command on hosts ENDPOINT_ID

EndpointID Hostname StartDate EndDate Error
6942a43afe8d4e463ca5c197 ENDPOINT_NAME 2025-12-16T22:06:33Z 2025-12-16T22:06:33Z Success

gz-endpoint-deisolate


Restores an isolated endpoint to the network.

Base Command

gz-endpoint-deisolate

Input

Argument Name Description Required
id The ID of the endpoint to restore from isolation. Required

Context Output

Path Type Description
GravityZone.Command.Deisolate.TaskID String The task ID.
GravityZone.Command.Deisolate.TaskType String The task type.
GravityZone.Command.Deisolate.EndpointID String The endpoint ID.
GravityZone.Command.Deisolate.Hostname String The endpoint name.
GravityZone.Command.Deisolate.Status String The command status.
GravityZone.Command.Deisolate.ErrorCode String The command error code, or “Success”.
GravityZone.Command.Deisolate.Error String The command error message, or “Success”.
GravityZone.Command.Deisolate.StartDate Date The start date of the command execution.
GravityZone.Command.Deisolate.EndDate Date The end date of the command execution.

Command example

!gz-endpoint-deisolate id=6942a43afe8d4e463ca5c197

Context Example

{
    "GravityZone": {
        "Command" : {
            "Deisolate" : {
                "TaskID": "6941bbc98ba450a5c10e5a16",
                "TaskType": "Deisolate",
                "Status": "Processed",
                "EndDate": "2025-12-16T22:08:33Z",
                "EndpointID": "6942a43afe8d4e463ca5c197",
                "Hostname": "ENDPOINT_NAME",
                "ErrorCode": "Success",
                "Error": "Success",
                "StartDate": "2025-12-16T22:08:33Z"
            }
        }
    }
}

Human Readable Output

GravityZone.Command.Deisolate command on hosts ENDPOINT_ID

EndpointID Hostname StartDate EndDate Error
6942a43afe8d4e463ca5c197 ENDPOINT_NAME 2025-12-16T22:08:33Z 2025-12-16T22:08:33Z Success

gz-endpoint-kill-process


Terminates a process on an endpoint by process ID.

Base Command

gz-endpoint-kill-process

Input

Argument Name Description Required
id The endpoint ID. Required
pid The ID of the process to kill. Required

Context Output

Path Type Description
GravityZone.Command.KillProcess.TaskID String The task ID.
GravityZone.Command.KillProcess.TaskType String The task type.
GravityZone.Command.KillProcess.EndpointID String The endpoint ID.
GravityZone.Command.KillProcess.Hostname String The endpoint name.
GravityZone.Command.KillProcess.Status String The command status.
GravityZone.Command.KillProcess.ErrorCode String The command error code, or “Success”.
GravityZone.Command.KillProcess.Error String The command error message, or “Success”.
GravityZone.Command.KillProcess.StartDate Date The start date of the command execution.
GravityZone.Command.KillProcess.EndDate Date The end date of the command execution.
GravityZone.Command.KillProcess.ProcessID Number The process ID.
GravityZone.Command.KillProcess.ProcessPath String The process path.

Command example

!gz-endpoint-kill-process id=6942a43afe8d4e463ca5c197 pid=5876

Context Example

{
    "GravityZone": {
        "Command": {
            "KillProcess": {
                "TaskID": "6941b6ffa830c3132b0d63d8",
                "TaskType": "KillProcess",
                "Status": "Processed",
                "EndDate": "2025-12-16T21:46:08Z",
                "EndpointID": "6942a43afe8d4e463ca5c197",
                "Hostname": "ENDPOINT_NAME",
                "ErrorCode": "Success",
                "Error": "Success",
                "StartDate": "2025-12-16T21:46:07Z",
                "ProcessID": 5876,
                "ProcessPath": ""
            }
        }
    }
}

Human Readable Output

GravityZone.Command.KillProcess command on hosts ENDPOINT_ID

EndpointID Hostname StartDate EndDate Error ProcessID
6942a43afe8d4e463ca5c197 ENDPOINT_NAME 2025-12-16T21:46:07Z 2025-12-16T21:46:08Z Success 5876

gz-endpoint-run-command


Runs a command on the endpoint. The applied policy must have Remote Shell enabled for this action to work.

Base Command

gz-endpoint-run-command

Input

Argument Name Description Required
id The ID of the endpoint on which to run the command. Required
command The command to run on the endpoint. Required

Context Output

Path Type Description
GravityZone.Command.RunCommand.ActivityID String The activity ID.
GravityZone.Command.RunCommand.ActivityType String The activity type.
GravityZone.Command.RunCommand.Status String The command status.
GravityZone.Command.RunCommand.EndpointID String The endpoint ID.
GravityZone.Command.RunCommand.ErrorCode String The command error code, or “Success”.
GravityZone.Command.RunCommand.Output String The command output.
GravityZone.Command.RunCommand.FileID String The stored file ID, when the command output cannot be extracted as an archive.
GravityZone.Command.RunCommand.FileName String The stored file name, when the command output cannot be extracted as an archive.

Command example

!gz-endpoint-run-command id=6942a43afe8d4e463ca5c197 command=whoami

Context Example

{
    "GravityZone": {
        "Command": {
            "RunCommand": {
                "ActivityID": "6943e3391e4fe5e8a40e51f6",
                "ActivityType": "RunCommand",
                "Status": "Success",
                "EndpointID": "6942a43afe8d4e463ca5c197",
                "ErrorCode": null,
                "Command": "whoami",
                "Output": "root"
            }
        }
    }
}

Human Readable Output

GravityZone.Command.RunCommand command on host 6942a43afe8d4e463ca5c197

EndpointID Status ErrorCode Command Output
6942a43afe8d4e463ca5c197 Success   whoami root

gz-endpoint-upload-file


Uploads a file to an endpoint.

Base Command

gz-endpoint-upload-file

Input

Argument Name Description Required
id The ID of the target endpoint for the file upload. Required
entry_id The entry ID of the file to upload. This file needs to exist in Cortex XSOAR. Required
remote_location The full folder path on the endpoint where the file will be uploaded. Required

Context Output

Path Type Description
GravityZone.Command.UploadFile.TaskID String The task ID.
GravityZone.Command.UploadFile.TaskType String The task type.
GravityZone.Command.UploadFile.EndpointID String The endpoint ID.
GravityZone.Command.UploadFile.Hostname String The endpoint name.
GravityZone.Command.UploadFile.Status String The command status.
GravityZone.Command.UploadFile.ErrorCode String The command error code, or “Success”.
GravityZone.Command.UploadFile.Error String The command error message, or “Success”.
GravityZone.Command.UploadFile.StartDate Date The start date of the command execution.
GravityZone.Command.UploadFile.EndDate Date The end date of the command execution.
GravityZone.Command.UploadFile.EntryID String The entry ID of the uploaded file.
GravityZone.Command.UploadFile.DestinationPath String The destination path of the uploaded file.

Command example

!gz-endpoint-upload-file id=6942a43afe8d4e463ca5c197 entry_id=LtvQ6B8eCFvTNo7bf3vDUD@25729aa7-7442-4231-8b98-ecb0fc29a642 remote_location=/root/test/

Context Example

{
    "GravityZone": {
        "Command": {
            "UploadFile": {
                "TaskID": "694447384f1ba9a2650ec75a",
                "TaskType": "UploadFile",
                "Status": "Processed",
                "EndDate": "2025-12-18T20:26:40Z",
                "EndpointID": "6942a43afe8d4e463ca5c197",
                "Hostname": "ENDPOINT_NAME",
                "ErrorCode": "Success",
                "Error": "Success",
                "StartDate": "2025-12-18T20:26:00Z",
                "EntryID": "LtvQ6B8eCFvTNo7bf3vDUD@25729aa7-7442-4231-8b98-ecb0fc29a642",
                "DestinationPath": "/root/test/"
            }
        }
    }
}

Human Readable Output

GravityZone.Command.UploadFile command on hosts 6942a43afe8d4e463ca5c197

EndpointID Hostname StartDate EndDate Error EntryID DestinationPath
6942a43afe8d4e463ca5c197 ENDPOINT_NAME 2025-12-18T20:26:00Z 2025-12-18T20:26:40Z Success LtvQ6B8eCFvTNo7bf3vDUD@25729aa7-7442-4231-8b98-ecb0fc29a642 /root/test/

gz-endpoint-list-by-running-process-hash


Retrieves endpoints that are running processes with a specified hash. The API key and the applied policy must allow Live Search. Endpoints must be online. The command waits up to five minutes for responses before timing out.

Base Command

gz-endpoint-list-by-running-process-hash

Input

Argument Name Description Required
process_hash Hash of the process. Required

Context Output

Path Type Description
GravityZone.Command.EndpointsRunningProcessHash.EndpointID String The endpoint ID.
GravityZone.Command.EndpointsRunningProcessHash.Path String The Live Search results.
GravityZone.Command.EndpointsRunningProcessHash.ProcessHash String The process hash.

Command example

!gz-endpoint-list-by-running-process-hash process_hash=b8412bcc6f47b2a11e4c39bc1bc9fab74969ff0648ba22db4c5254a0457af0c7

Context Example

{
    "GravityZone": {
        "Command": {
            "EndpointsRunningProcessHash": {
                "EndpointID": "6942a43afe8d4e463ca5c197",
                "Path": "/opt/bitdefender-security-tools/bin/epagngd",
                "ProcessHash": "b8412bcc6f47b2a11e4c39bc1bc9fab74969ff0648ba22db4c5254a0457af0c7"
            }
        }
    }
}

Human Readable Output

Live Search Results

EndpointID ProcessHash Results
6942a43afe8d4e463ca5c197 b8412bcc6f47b2a11e4c39bc1bc9fab74969ff0648ba22db4c5254a0457af0c7 {“hostname”:”ENDPOINT_NAME”,”path”:”/opt/bitdefender-security-tools/bin/epagngd”}

gz-endpoint-get-process-tree-by-hash


Retrieves the process tree on an endpoint for a specified process hash. The applied policy must allow Live Search, and the endpoint must be online.

Base Command

gz-endpoint-get-process-tree-by-hash

Input

Argument Name Description Required
id The ID of the targeted endpoint. Required
process_hash The hash of the process to search for. Required

Context Output

Path Type Description
GravityZone.Command.ProcessTreeForHash.EndpointID String The endpoint ID.
GravityZone.Command.ProcessTreeForHash.PID Number The process ID.
GravityZone.Command.ProcessTreeForHash.Path String The process path.
GravityZone.Command.ProcessTreeForHash.Cmdline String The command line used to start the process.
GravityZone.Command.ProcessTreeForHash.ParentPID Number The parent process ID.

Command example

!gz-endpoint-get-process-tree-by-hash id=6942a43afe8d4e463ca5c197 process_hash=b8412bcc6f47b2a11e4c39bc1bc9fab74969ff0648ba22db4c5254a0457af0c7

Context Example

{
    "GravityZone": {
        "Command": {
            "ProcessTreeForHash": {
                "EndpointID": "6942a43afe8d4e463ca5c197",
                "Cmdline": "/usr/lib/systemd/systemd --system --deserialize=73",
                "ParentPID": 0,
                "Path": "/usr/lib/systemd/systemd",
                "PID": 1
            }
        }
    }
}

Human Readable Output

Live Search Results

EndpointID ProcessHash Results
6942a43afe8d4e463ca5c197 b8412bcc6f47b2a11e4c39bc1bc9fab74969ff0648ba22db4c5254a0457af0c7 {“cmdline”:”/usr/lib/systemd/systemd –system –deserialize=73”,”parent”:”0”,”path”:”/usr/lib/systemd/systemd”,”pid”:”1”}

gz-incident-get


Retrieves incident details by ID.

Base Command

gz-incident-get

Input

Argument Name Description Required
id The ID of the incident to fetch. Required

Context Output

Path Type Description
GravityZone.Incident.ID String The incident ID.
GravityZone.Incident.Type String The incident type (‘incident’ / ‘extendedIncident’).
GravityZone.Incident.Company.Name String The GravityZone company name where the incident is located.
GravityZone.Incident.Company.ID String The GravityZone company ID where the incident is located.
GravityZone.Incident.Number Number The incident number (specific to the company).
GravityZone.Incident.Severity.Score Number The incident severity score (0 - 100).
GravityZone.Incident.Status Number The incident status.
GravityZone.Incident.ActionTaken String The action taken by the detecting technology (‘reported’ / ‘blocked’ / ‘partially_blocked’).
GravityZone.Incident.Created Date The date when the incident was detected on the endpoint / sensor.
GravityZone.Incident.LastUpdated Date The date when the incident was last updated in GravityZone or by the endpoint / sensor.
GravityZone.Incident.LastProcessed Date The date when the incident was last processed by GravityZone services.
GravityZone.Incident.Permalink String The incident URL in the GravityZone Console.
GravityZone.Incident.AssignedPriority String The priority assigned to the incident (‘unknown’ / ‘low’ / ‘medium’ / ‘high’ / ‘critical’).
GravityZone.Incident.Notes.Text String The note contents.
GravityZone.Incident.Notes.User String The username (email) of the user who wrote the note.
GravityZone.Incident.Notes.Date Date The note creation date.
GravityZone.Incident.Alerts.Name String The alert name.
GravityZone.Incident.Alerts.Date Date The date of when the alert was generated.
GravityZone.Incident.Alerts.Sensors String The list of sensors that generated the alert. Available only for ‘extendedIncident’-type incidents.
GravityZone.Incident.Alerts.Tactic String The list of tactics that generated the alert. Available only for ‘extendedIncident’-type incidents.
GravityZone.Incident.Alerts.Resources Array The list of resources involved in the ‘incident’-type incidents.
GravityZone.Incident.RawJSON JSON The raw JSON response from the GravityZone API.
GravityZone.Incident.EndpointID String The endpoint ID. Available only for ‘incident’-type incidents.
GravityZone.Incident.EndpointHostname String The endpoint hostname. Available only for ‘incident’-type incidents.
GravityZone.Incident.EndpointIP String The endpoint IP address. Available only for ‘incident’-type incidents.

Command example

!gz-incident-get id=69415c9d5f5c8b75247c58d1

Context Example

{
    "GravityZone": {
        "Incident": {
            "ActionTaken": "blocked",
            "Alerts": [
                {
                    "Date": "2025-12-16T15:20:19+02:00",
                    "DetectedBy": {
                        "Class": "EDR Detection",
                        "Name": "KeyloggingAPICall"
                    },
                    "Name": "KeyloggingAPICall",
                    "Resources": [
                        {
                            "CommandLine": "\"C:\\Windows\\System32\\notepad.exe\" C:\\Users\\bdvm\\Desktop\\New Text Document.txt",
                            "ParentPid": 5212,
                            "ParentProcessAccessPrivileges": "elevated",
                            "ParentProcessIntegrityLevel": "high",
                            "ParentProcessPath": "c:\\windows\\explorer.exe",
                            "ParentProcessUser": "TA66H0I6F225\\bdvm",
                            "Pid": 2592,
                            "ProcessAccessPrivileges": "elevated",
                            "ProcessIntegrityLevel": "high",
                            "ProcessPath": "c:\\windows\\system32\\notepad.exe",
                            "Type": "process",
                            "User": "TA66H0I6F225\\bdvm"
                        }
                    ]
                }
            ],
            "AssignedPriority": "unknown",
            "AssignedUser": null,
            "Company": {
                "ID": "611717cb22a30cee251b03f3",
                "Name": "Company"
            },
            "Created": "2025-12-16T15:20:21+02:00",
            "ID": "69415c9d5f5c8b75247c58d1",
            "LastProcessed": "2025-12-16T15:21:48+02:00",
            "LastUpdated": "2025-12-16T15:21:38+02:00",
            "Notes": [
                {
                    "Date": "2026-02-23T13:27:53+02:00",
                    "Text": "InvestigationInProgress",
                    "User": "user email"
                }
            ],
            "Number": 473,
            "Permalink": "https://gravityzone.domain/#!/incidents/view/69415c9d5f5c8b75247c58d1",
            "EndpointID" : "694148bffe8d4e463ca5bd1e",
            "EndpointHostname": "TA66H0I6F225",
            "EndpointIP": "1.1.1.1",
            "RawJSON": {
                "assignee": null,
                "attackTypes": [
                    "Malware"
                ],
                "company": {
                    "id": "611717cb22a30cee251b03f3",
                    "name": "Company"
                },
                "created": "2025-12-16T15:20:21+02:00",
                "details": {
                    "alerts": [
                        {
                            "date": "2025-12-16T15:20:19+02:00",
                            "detectedBy": {
                                "class": "EDR Detection",
                                "name": "KeyloggingAPICall"
                            },
                            "extra": [
                                {
                                    "key": "hookedApiName",
                                    "value": "Key Logging"
                                },
                                {
                                    "key": "extraInfo1",
                                    "value": "\nProcess PE VersionInfo and Certification Information: \nOriginal File Name: NOTEPAD.EXE\nInternal Name: Notepad\nFile Description: Notepad\nCompany Name: Microsoft Corporation\nFile Version: 10.0.19041.1865 (WinBuild.160101.0800)\nProduct Name: MicrosoftR WindowsR Operating System\nProduct Version: 10.0.19041.1865\nLegal Copyright: C Microsoft Corporation. All rights reserved.\nCertificate Serial: 330000033b655faefadb75e9d600000000033b\nCertificate Signer: Microsoft Corporation\nCertificate Issuer: Microsoft Corporation\n\nWorking Directory: c:\\users\\bdvm\\desktop\\"
                                }
                            ],
                            "id": "69415ce1997e9c48ed12c360",
                            "name": "KeyloggingAPICall",
                            "resources": [
                                {
                                    "details": {
                                        "commandLine": "\"C:\\Windows\\System32\\notepad.exe\" C:\\Users\\bdvm\\Desktop\\New Text Document.txt",
                                        "loadedModule": null,
                                        "loadedModulePid": null,
                                        "parentPid": 5212,
                                        "parentProcessAccessPrivileges": "elevated",
                                        "parentProcessCmdline": null,
                                        "parentProcessIntegrityLevel": "high",
                                        "parentProcessPath": "c:\\windows\\explorer.exe",
                                        "parentProcessUser": "TA66H0I6F225\\bdvm",
                                        "pid": 2592,
                                        "processAccessPrivileges": "elevated",
                                        "processInjectionSizeofWrite": null,
                                        "processInjectionTarget": null,
                                        "processInjectionTargetPid": null,
                                        "processInjectionWriter": null,
                                        "processInjectionWriterPid": null,
                                        "processIntegrityLevel": "high",
                                        "processPackerName": null,
                                        "processPath": "c:\\windows\\system32\\notepad.exe",
                                        "processPathSize": null,
                                        "user": "TA66H0I6F225\\bdvm"
                                    },
                                    "type": "process"
                                }
                            ]
                        }
                    ],
                    "computerFqdn": "ta66h0i6f225",
                    "computerId": "694148bffe8d4e463ca5bd1e",
                    "computerIp": "1.1.1.1",
                    "computerMacAddresses": [
                        "005056a7862b"
                    ],
                    "computerName": "TA66H0I6F225",
                    "counters": {
                        "domains": 0,
                        "endpoints": 1,
                        "events": 17,
                        "files": 3,
                        "processes": 4,
                        "registries": 0,
                        "storages": 0
                    },
                    "detectionName": "BAT.Trojan.FormatC.Z",
                    "mitreTags": [
                        {
                            "category": "Execution",
                            "techniques": [
                                {
                                    "id": "T1059",
                                    "name": "Command and Scripting Interpreter",
                                    "subtechniques": null
                                }
                            ]
                        }
                    ],
                    "nodes": [
                        {
                            "alertIds": [
                                "69415ce1997e9c48ed12c361"
                            ],
                            "details": {
                                "file": {
                                    "isExecutable": true,
                                    "md5": null,
                                    "name": "<system>",
                                    "path": "<system>",
                                    "sha256": null,
                                    "size": 0
                                },
                                "killProcess": null,
                                "process": {
                                    "commandLine": "<did_not_receive>",
                                    "date": "2025-12-16T14:05:30+02:00",
                                    "name": "<system>",
                                    "parent": {
                                        "name": "<SYSTEM>",
                                        "path": null,
                                        "pid": 0
                                    },
                                    "pid": 0,
                                    "userId": null,
                                    "userName": "NT AUTHORITY\\SYSTEM"
                                },
                                "quarantine": null,
                                "sandbox": null
                            },
                            "id": "69415c9d5f5c8b75247c58e4",
                            "name": "<system>",
                            "type": "process_execution"
                        }
                    ],
                    "partOf": null,
                    "transitions": [
                        {
                            "date": "2025-12-16T15:19:59+02:00",
                            "from": "69415c9d5f5c8b75247c58e2",
                            "to": "69415c9d5f5c8b75247c58e5"
                        }
                    ],
                    "triggerNodeId": "69415c9d5f5c8b75247c58e5"
                },
                "incidentId": "69415c9d5f5c8b75247c58d1",
                "incidentLink": "https://gravityzone.domain/#!/incidents/view/69415c9d5f5c8b75247c58d1",
                "incidentNumber": 473,
                "incidentType": "incident",
                "lastProcessed": "2025-12-16T15:21:48+02:00",
                "lastUpdated": "2025-12-16T15:21:38+02:00",
                "mainAction": "blocked",
                "notes": [
                    {
                        "created": "2026-02-23T13:27:53+02:00",
                        "id": "699c39b9cf8645f670042eb0",
                        "text": "InvestigationInProgress",
                        "userId": "6941466eda1158ec9f0ecb5d",
                        "userName": "user email"
                    }
                ],
                "priority": "unknown",
                "severityScore": 43,
                "status": "open"
            },
            "Severity": {
                "Score": 43
            },
            "Status": 0,
            "Type": "incident"
        }
    }
}

Human Readable Output

GravityZone Incident

Action Taken Assigned Priority Assigned User Company Name Created Endpoint ID Endpoint IP Endpoint Name ID Last Processed Last Updated Number Permalink Severity Score Status Type
Blocked Unknown Unassigned Company 2025-12-16T15:20:21+02:00 694148bffe8d4e463ca5bd1e 1.1.1.1 TA66H0I6F225 69415c9d5f5c8b75247c58d1 2025-12-16T15:21:48+02:00 2025-12-16T15:21:38+02:00 473 https://gravityzone.domain/#!/incidents/view/69415c9d5f5c8b75247c58d1 43% 0 (Pending) Incident (EDR)

Incident Notes

Text User Date
InvestigationInProgress user email 2026-02-23T13:27:53+02:00

Incident Alerts

Date Detected By Name Resources
2025-12-16T15:20:19+02:00 KeyloggingAPICall (EDR Detection) KeyloggingAPICall [{“Pid”: 2592,”ProcessPath”: “c:\windows\system32\notepad.exe”,”CommandLine”: “"C:\Windows\System32\notepad.exe" C:\Users\bdvm\Desktop\New Text Document.txt”,”ParentPid”: 5212,”ParentProcessPath”: “c:\windows\explorer.exe”,”ParentProcessUser”: “TA66H0I6F225\bdvm”,”User”: “TA66H0I6F225\bdvm”,”ProcessAccessPrivileges”: “elevated”,”ParentProcessAccessPrivileges”: “elevated”,”ProcessIntegrityLevel”: “high”,”ParentProcessIntegrityLevel”: “high”,”Type”: “process”}]

gz-incident-list


Retrieves incidents within the last three days from all endpoints or a specific endpoint.

Base Command

gz-incident-list

Input

Argument Name Description Required
from_date The start date to retrieve incidents from. The default value is the current date minus three days. Optional
to_date The end date to retrieve incidents until. The default value is the current date and time. Optional
limit The maximum number of incidents to retrieve. Optional
endpoint_id The ID of the endpoint to list incidents for. Optional

Context Output

Path Type Description
GravityZone.IncidentsList.ID String The incident ID.
GravityZone.IncidentsList.Type String The incident type (‘incident’ / ‘extendedIncident’).
GravityZone.IncidentsList.CompanyName String The GravityZone company name where the incident is located.
GravityZone.IncidentsList.Number Number The incident number (specific to the company).
GravityZone.IncidentsList.SeverityScore Number The incident severity score (0 - 100).
GravityZone.IncidentsList.Status Number The incident status.
GravityZone.IncidentsList.ActionTaken String The action taken by the detecting technology (‘reported’ / ‘blocked’ / ‘partially_blocked’).
GravityZone.IncidentsList.Created Date The date when the incident was detected on the endpoint / sensor.
GravityZone.IncidentsList.LastUpdated Date The date when the incident was last updated in GravityZone or by the endpoint / sensor.
GravityZone.IncidentsList.LastProcessed Date The incident last processed date by GravityZone services.
GravityZone.IncidentsList.Permalink String The incident URL in the GravityZone Console.
GravityZone.IncidentsList.AssignedPriority String The priority assigned to the incident (‘unknown’ / ‘low’ / ‘medium’ / ‘high’ / ‘critical’).
GravityZone.IncidentsList.AssignedUserId String The ID of the user assigned to the incident.
GravityZone.IncidentsList.RawJSON JSON The raw JSON response from the GravityZone API.
GravityZone.IncidentsList.EndpointID String The endpoint ID. Available only for ‘incident’-type incidents.
GravityZone.IncidentsList.EndpointHostname String The endpoint hostname. Available only for ‘incident’-type incidents.
GravityZone.IncidentsList.EndpointIP String The endpoint IP address. Available only for ‘incident’-type incidents.

Command example


#### Context Example

```json
{
    "GravityZone": {
        "IncidentsList": [
            {
                "ActionTaken": "reported",
                "AssignedPriority": "unknown",
                "AssignedUserId": null,
                "AttackTypes": [
                    "Persistence"
                ],
                "CompanyID": "611717cb22a30cee251b03f3",
                "CompanyName": "Company",
                "Created": "2026-02-23T13:09:34+02:00",
                "ID": "699c367136732459d7d3b1af",
                "LastProcessed": "2026-02-23T13:13:53+02:00",
                "LastUpdated": "2026-02-23T13:09:34+02:00",
                "Number": 1017,
                "Permalink": "https://gravityzone.domain/#!/incidents/view/699c367136732459d7d3b1af",
                "RawJSON": {
                    "assignee": null,
                    "attackTypes": [
                        "Persistence"
                    ],
                    "company": {
                        "id": "611717cb22a30cee251b03f3",
                        "name": "Company"
                    },
                    "created": "2026-02-23T13:09:34+02:00",
                    "details": {
                        "contains": null,
                        "counters": {
                            "ADInstances": 0,
                            "AWSInstances": 0,
                            "DGAs": 0,
                            "DNSs": 0,
                            "GCPInstances": 0,
                            "IPs": 0,
                            "IoTs": 0,
                            "atlassianBitbucketProducts": 0,
                            "atlassianConfluenceProducts": 0,
                            "atlassianInstances": 0,
                            "atlassianJiraProducts": 0,
                            "azureADInstances": 1,
                            "bitbucketProjects": 0,
                            "cloudStorages": 0,
                            "confluenceSpaces": 0,
                            "containers": 0,
                            "databases": 0,
                            "domains": 0,
                            "emails": 0,
                            "endpoints": 0,
                            "exfiltratedFiles": 0,
                            "externalDrives": 0,
                            "externalSources": 0,
                            "googleWorkspaceInstances": 0,
                            "identities": 1,
                            "internalEmails": 0,
                            "internalIPs": 0,
                            "mobileDevices": 0,
                            "office365Instances": 0,
                            "printers": 0,
                            "routers": 0,
                            "servers": 0,
                            "storages": 0,
                            "torNodes": 0,
                            "users": 1,
                            "virtualDesktops": 0
                        },
                        "partOf": null
                    },
                    "incidentId": "699c367136732459d7d3b1af",
                    "incidentLink": "https://gravityzone.domain/#!/incidents/view/699c367136732459d7d3b1af",
                    "incidentNumber": 1017,
                    "incidentType": "extendedIncident",
                    "lastProcessed": "2026-02-23T13:13:53+02:00",
                    "lastUpdated": "2026-02-23T13:09:34+02:00",
                    "mainAction": "reported",
                    "priority": "unknown",
                    "severityScore": 71,
                    "status": "in_progress"
                },
                "SeverityScore": 71,
                "Status": 1,
                "Type": "extendedIncident"
            }
        ]
    }
}

Human Readable Output

GravityZone Incidents List

ActionTaken Assigned Priority Assigned User ID Attack Types Company Name Created ID Last Processed Last Updated Number Permalink Severity Score Status Type
Reported Unknown Unassigned Persistence Company 2026-02-23T13:09:34+02:00 699c367136732459d7d3b1af 2026-02-23T13:13:53+02:00 2026-02-23T13:09:34+02:00 1017 https://gravityzone.domain/#!/incidents/view/699c367136732459d7d3b1af 71% 1 (Active) Extended Incident (XDR)

Command example

!gz-incident-list from_date="2026-02-20" to_date="2026-02-21" limit="1"

Context Example

{
    "GravityZone": {
        "IncidentsList": [
            {
                "ActionTaken": "reported",
                "AssignedPriority": "unknown",
                "AssignedUserId": null,
                "AttackTypes": [
                    "Persistence"
                ],
                "CompanyID": "611717cb22a30cee251b03f3",
                "CompanyName": "Company",
                "Created": "2026-02-20T20:58:17+02:00",
                "ID": "6998b40f36732459d7d3aa58",
                "LastProcessed": "2026-02-20T21:20:47+02:00",
                "LastUpdated": "2026-02-20T20:58:17+02:00",
                "Number": 998,
                "Permalink": "https://gravityzone.domain/#!/incidents/view/6998b40f36732459d7d3aa58",
                "RawJSON": {
                    "assignee": null,
                    "attackTypes": [
                        "Persistence"
                    ],
                    "company": {
                        "id": "611717cb22a30cee251b03f3",
                        "name": "Company"
                    },
                    "created": "2026-02-20T20:58:17+02:00",
                    "details": {
                        "contains": null,
                        "counters": {
                            "ADInstances": 0,
                            "AWSInstances": 0,
                            "DGAs": 0,
                            "DNSs": 0,
                            "GCPInstances": 0,
                            "IPs": 0,
                            "IoTs": 0,
                            "atlassianBitbucketProducts": 0,
                            "atlassianConfluenceProducts": 0,
                            "atlassianInstances": 0,
                            "atlassianJiraProducts": 0,
                            "azureADInstances": 1,
                            "bitbucketProjects": 0,
                            "cloudStorages": 0,
                            "confluenceSpaces": 0,
                            "containers": 0,
                            "databases": 0,
                            "domains": 0,
                            "emails": 0,
                            "endpoints": 0,
                            "exfiltratedFiles": 0,
                            "externalDrives": 0,
                            "externalSources": 0,
                            "googleWorkspaceInstances": 0,
                            "identities": 1,
                            "internalEmails": 0,
                            "internalIPs": 0,
                            "mobileDevices": 0,
                            "office365Instances": 0,
                            "printers": 0,
                            "routers": 0,
                            "servers": 0,
                            "storages": 0,
                            "torNodes": 0,
                            "users": 1,
                            "virtualDesktops": 0
                        },
                        "partOf": null
                    },
                    "incidentId": "6998b40f36732459d7d3aa58",
                    "incidentLink": "https://gravityzone.domain/#!/incidents/view/6998b40f36732459d7d3aa58",
                    "incidentNumber": 998,
                    "incidentType": "extendedIncident",
                    "lastProcessed": "2026-02-20T21:20:47+02:00",
                    "lastUpdated": "2026-02-20T20:58:17+02:00",
                    "mainAction": "reported",
                    "priority": "unknown",
                    "severityScore": 71,
                    "status": "open"
                },
                "SeverityScore": 71,
                "Status": 0,
                "Type": "extendedIncident"
            }
        ]
    }
}

Human Readable Output

GravityZone Incidents List

ActionTaken Assigned Priority Assigned User ID Attack Types Company Name Created ID Last Processed Last Updated Number Permalink Severity Score Status Type
Reported Unknown Unassigned Persistence Company 2026-02-20T20:58:17+02:00 6998b40f36732459d7d3aa58 2026-02-20T21:20:47+02:00 2026-02-20T20:58:17+02:00 998 http://gravityzone.domain/#!/incidents/view/6998b40f36732459d7d3aa58 71% 0 (Pending) Extended Incident (XDR)

Command example

!gz-incident-list endpoint_id=694148bffe8d4e463ca5bd1e from_date="2025-11-20"

Context Example

{
    "GravityZone": {
        "IncidentsList": [
            {
                "ActionTaken": "reported",
                "AssignedPriority": "critical",
                "AssignedUserId": "6540b72b1ffbc14e1808bc00",
                "AttackTypes": [
                    "Malware"
                ],
                "CompanyID": "611717cb22a30cee251b03f3",
                "CompanyName": "Company",
                "Created": "2025-12-17T09:02:03+02:00",
                "ID": "694255755f5c8b75247c7ce2",
                "LastProcessed": "2025-12-17T09:02:13+02:00",
                "LastUpdated": "2025-12-17T09:02:03+02:00",
                "Number": 477,
                "Permalink": "https://gravityzone.domain/#!/incidents/view/694255755f5c8b75247c7ce2",
                "EndpointID" : "694148bffe8d4e463ca5bd1e",
                "EndpointHostname": "TA66H0I6F225",
                "EndpointIP": "1.1.1.1",
                "RawJSON": {
                    "assignee": "6540b72b1ffbc14e1808bc00",
                    "attackTypes": [
                        "Malware"
                    ],
                    "company": {
                        "id": "611717cb22a30cee251b03f3",
                        "name": "Company"
                    },
                    "created": "2025-12-17T09:02:03+02:00",
                    "details": {
                        "computerFqdn": "ta66h0i6f225",
                        "computerId": "694148bffe8d4e463ca5bd1e",
                        "computerIp": "1.1.1.1",
                        "computerMacAddresses": [
                            "005056a7862b"
                        ],
                        "computerName": "TA66H0I6F225",
                        "counters": {
                            "domains": 0,
                            "endpoints": 1,
                            "events": 6,
                            "files": 0,
                            "processes": 2,
                            "registries": 0,
                            "storages": 0
                        },
                        "detectionName": "RegSecurityDump",
                        "partOf": null
                    },
                    "incidentId": "694255755f5c8b75247c7ce2",
                    "incidentLink": "https://gravityzone.domain/#!/incidents/view/694255755f5c8b75247c7ce2",
                    "incidentNumber": 477,
                    "incidentType": "incident",
                    "lastProcessed": "2025-12-17T09:02:13+02:00",
                    "lastUpdated": "2025-12-17T09:02:03+02:00",
                    "mainAction": "reported",
                    "priority": "critical",
                    "severityScore": 33,
                    "status": "closed"
                },
                "SeverityScore": 33,
                "Status": 2,
                "Type": "incident"
            }
        ]
    }
}

Human Readable Output

GravityZone Incidents List

ActionTaken Assigned Priority Assigned User ID Attack Types Company Name Created Endpoint ID Endpoint IP Endpoint Name ID Last Processed Last Updated Number Permalink Severity Score Status Type
Reported Critical 6540b72b1ffbc14e1808bc00 Malware Company 2025-12-17T09:02:03+02:00 694148bffe8d4e463ca5bd1e 1.1.1.1 TA66H0I6F225 694255755f5c8b75247c7ce2 2025-12-17T09:02:13+02:00 2025-12-17T09:02:03+02:00 477 https://gravityzone.domain/#!/incidents/view/694255755f5c8b75247c7ce2 33% 2 (Done) Incident (EDR)

gz-incident-add-note


Adds a note to one or more incidents.

Base Command

gz-incident-add-note

Input

Argument Name Description Required
id The IDs of the incidents to add a note to. Supports comma separated values. Required
note The note to add to the incidents. Required

Context Output

Path Type Description
GravityZone.Command.AddIncidentNote.IncidentID String The incident ID.
GravityZone.Command.AddIncidentNote.Note String The requested note.
GravityZone.Command.AddIncidentNote.CommandStatus String The command status.

Command example

!gz-incident-add-note id=69415c9d5f5c8b75247c58d1 note=InvestigationInProgress

Context Example

{
    "GravityZone": {
        "Command": {
            "AddIncidentNote": {
                "CommandStatus": "Success",
                "IncidentID": "69415c9d5f5c8b75247c58d1",
                "Note": "InvestigationInProgress"
            }
        }
    }
}

Human Readable Output

GravityZone.Command.AddIncidentNote command on incidents 69415c9d5f5c8b75247c58d1

IncidentID Note CommandStatus
69415c9d5f5c8b75247c58d1 InvestigationInProgress Success

gz-incident-change-status


Changes the status of one or more incidents.

Base Command

gz-incident-change-status

Input

Argument Name Description Required
id The IDs of the incidents to update. Supports comma separated values. Required
status The new status of the incidents. Possible values are: PENDING, ACTIVE, DONE, ARCHIVE. Required

Context Output

Path Type Description
GravityZone.Command.ChangeIncidentStatus.IncidentID String The incident ID.
GravityZone.Command.ChangeIncidentStatus.IncidentStatus String The requested status of the incident.
GravityZone.Command.ChangeIncidentStatus.CommandStatus String The command status.

Command example

!gz-incident-change-status id=69415c9d5f5c8b75247c58d1 status=ACTIVE

Context Example

{
    "GravityZone": {
        "Command": {
            "ChangeIncidentStatus": {
                "CommandStatus": "Success",
                "IncidentID": "69415c9d5f5c8b75247c58d1",
                "IncidentStatus": "ACTIVE"
            }
        }
    }
}

Human Readable Output

GravityZone.Command.ChangeIncidentStatus command on incidents 69415c9d5f5c8b75247c58d1

IncidentID IncidentStatus CommandStatus
69415c9d5f5c8b75247c58d1 ACTIVE Success

gz-poll-investigation-activity-status


Checks the status of an investigation activity. This command is not intended for direct use.

Base Command

gz-poll-investigation-activity-status

Input

Argument Name Description Required
target_id The endpoint ID. Required
activity_id The activity ID. Required
output_file The path of the output file. Optional
metadata The metadata to pass to the task. Optional

Context Output

There is no context output for this command.

gz-poll-live-search-status


Checks the status of a Live Search query. This command is not intended for direct use.

Base Command

gz-poll-live-search-status

Input

Argument Name Description Required
task_id The task ID. Required
search_type The Live Search query type. Optional
metadata The metadata to pass to the task. Optional

Context Output

There is no context output for this command.

gz-poll-task-status


Checks the status of a task. This command is not intended for direct use.

Base Command

gz-poll-task-status

Input

Argument Name Description Required
task_id The task ID. Required
metadata The metadata to pass to the task. Optional

Context Output

There is no context output for this command.

get-modified-remote-data


Retrieves incidents that were modified since the last data sync. This command is intended for debugging purposes.

Base Command

get-modified-remote-data

Input

Argument Name Description Required
lastUpdate The time when the incident was last updated. Required

Context Output

There is no context output for this command.

get-remote-data


Retrieves data from a remote incident without updating the current incident. This command is intended for debugging purposes.

Base Command

get-remote-data

Input

Argument Name Description Required
id The ID of the incident to fetch. Required
lastUpdate The time when the incident was last updated. Required

Context Output

There is no context output for this command.

update-remote-system


Updates the remote incident with local incident changes without updating the current incident. This command is intended for debugging purposes.

Base Command

update-remote-system

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

get-mapping-fields


Returns the list of fields available for outgoing mirroring. This command is intended for debugging purposes only.

Base Command

get-mapping-fields

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

There is no context output for this command.

Incident Mirroring

You can enable incident mirroring between Cortex XSOAR incidents and GravityZone corresponding incidents (available from Cortex XSOAR version 6.0.0).
To set up the mirroring:

  1. Enable Fetching incidents in your instance configuration.
  2. In the Mirroring Direction integration parameter, select in which direction the incidents should be mirrored:

    Option Description
    None Disables incident mirroring.
    Incoming Reflects status changes from GravityZone incidents in Cortex XSOAR incidents.
    Outgoing Reflects status changes from Cortex XSOAR incidents in GravityZone incidents.
    Both Synchronizes status changes bidirectionally between Cortex XSOAR and GravityZone incidents.

Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.

gz-endpoint-users-loggedin


Retrieves details of all active user sessions on a specific managed endpoint.

Base Command

gz-endpoint-users-loggedin

Input

Argument Name Description Required
id The target endpoint ID. The endpoint must be accessible to you. Required

Context Output

Path Type Description
GravityZone.Endpoint.ID String The endpoint ID.
GravityZone.Endpoint.Hostname String The endpoint hostname.
GravityZone.Endpoint.ActiveSessions.Username String The username of the currently logged-in user.
GravityZone.Endpoint.ActiveSessions.ConnectionType String The connection type for the session.
GravityZone.Endpoint.ActiveSessions.StartTime Date The session start time, if available (e.g., “2021-05-19T10:37:56Z”).
GravityZone.Endpoint.ActiveSessions.UserSID String The Security Identifier (SID) of the logged-in user, if available.
GravityZone.Endpoint.ActiveSessions.DomainSID String The Security Identifier (SID) of the logged-in user’s domain, if available.
GravityZone.Endpoint.ActiveSessions.OrganizationalUnitDN String The full distinguished name (DN) of the organizational unit, if available.
GravityZone.Endpoint.ActiveSessions.MemberOfSIDs Array The SIDs for the groups to which the logged-in user belongs, if available.

Command example

!gz-endpoint-users-loggedin id=6942a43afe8d4e463ca5c197

Context Example

{
    "GravityZone": {
        "Endpoint": {
            "ID": "6942a43afe8d4e463ca5c197",
            "Hostname": "bdvm",
            "ActiveSessions": [
                {
                    "Username": "user@example.com",
                    "ConnectionType": "local",
                    "StartTime": "2021-05-19T10:37:56Z",
                    "UserSID": "S-1-5-21-1111111111-1111111111-1111111111-1001",
                    "DomainSID": "S-1-5-21-1111111111-1111111111-1111111111",
                    "OrganizationalUnitDN": "OU=Users,DC=example,DC=com",
                    "MemberOfSIDs": [
                        "S-1-5-32-544",
                        "S-1-5-32-545"
                    ]
                }
            ]
        }
    }
}

Human Readable Output

Active sessions on endpoint 6942a43afe8d4e463ca5c197

Username ConnectionType StartTime UserSID DomainSID OrganizationalUnitDN MemberOfSIDs
TESTDOMAIN\full.user local 2021-05-19T10:37:56Z S-1-2-25-33 S-1-23-131231 OU=office,OU=Developers,OU=Users,DC=microsoft,DC=com S-1-5-32-450
basic.user remote 2021-05-19T11:00:00Z        

gz-endpoint-create-memory-dump


Initiates the creation of a memory dump for the specified endpoint. Once the dump is successfully generated, the resulting archive is uploaded to the Bitdefender cloud.

Important:

  • Please contact Bitdefender Enterprise Support to configure this feature.
  • The resulting memory dump archive is automatically deleted from the Bitdefender cloud after 24 hours.
  • The memory dump creation task completes successfully only when the generated memory dump archive is 16 GB or smaller.

Base Command

gz-endpoint-create-memory-dump

Input

Argument Name Description Required
id The target endpoint ID. The endpoint must be accessible and run a Windows operating system. Required
path The full path to the destination folder on the specified endpoint where the memory dump archive will be created. Required
password The password used to secure the memory dump archive after it is created. It must contain at least 12 characters, including one uppercase letter, one lowercase letter, one digit, and one special character. Required
polling Whether the command waits for the memory dump task completion. If set to true, the command returns the final result after the task completes. If set to false, the command returns only the endpoint ID and task ID immediately after the task is initiated. Optional

Context Output

Path Type Description
GravityZone.MemoryDump.TaskID String The memory dump task ID.
GravityZone.MemoryDump.TaskType String The task type.
GravityZone.MemoryDump.Status String The memory dump task status.
GravityZone.MemoryDump.EndpointID String The endpoint ID.
GravityZone.MemoryDump.Hostname String The endpoint hostname.
GravityZone.MemoryDump.StartDate Date The date and time when the memory dump task started (e.g., “2026-05-25T10:00:00Z”).
GravityZone.MemoryDump.EndDate Date The date and time when the memory dump task ended, if available (e.g., “2026-05-25T10:01:00Z”).
GravityZone.MemoryDump.ErrorCode String An error code if the memory dump task fails, or “Success” if the task completes successfully.
GravityZone.MemoryDump.Error String An error message if the memory dump task fails, or “Success” if the task completes successfully.
GravityZone.MemoryDump.DownloadURL String The memory dump download URL, available after the task completes successfully.

Command example

!gz-endpoint-create-memory-dump id=6942a43afe8d4e463ca5c197 password="ComplexPass123!" path=C:\\test2

Context Example

{
    "GravityZone": {
        "MemoryDump": {
            "TaskID": "6836f4a01a2b3c4d5e6f7a8b",
            "TaskType": "CreateMemoryDump",
            "Status": "Processed",
            "EndpointID": "6942a43afe8d4e463ca5c197",
            "Hostname": "bdvm",
            "StartDate": "2026-05-25T10:00:00Z",
            "EndDate": "2026-05-25T10:01:00Z",
            "ErrorCode": "Success",
            "Error": "Success",
            "DownloadURL": "https://example.com/memory-dump.zip"
        }
    }
}

Human Readable Output

This example illustrates the human-readable output when polling is set to true. When polling is false, the table contains only the task ID and endpoint ID.

Memory dump for endpoint 6942a43afe8d4e463ca5c197

EndpointID Hostname StartDate EndDate Error DownloadURL
6942a43afe8d4e463ca5c197 bdvm 2026-05-25T10:00:00Z 2026-05-25T10:01:00Z Success https://example.com/memory-dump.zip

gz-endpoint-memory-dump-status


Verifies the status of a task initiated via gz-endpoint-create-memory-dump.

Base Command

gz-endpoint-memory-dump-status

Input

Argument Name Description Required
task_id The ID of the task started through gz-endpoint-create-memory-dump. This ID is returned upon task initiation. Required
endpoint_id The ID of the endpoint for which the specified task was initiated. Required
polling Whether the command waits for the gz-endpoint-create-memory-dump task completion. If set to true, the command returns the final result after the task completes successfully or fails. If set to false, the command returns immediately, providing only the task status. Optional

Context Output

Path Type Description
GravityZone.MemoryDump.TaskID String The ID of the task started through gz-endpoint-create-memory-dump.
GravityZone.MemoryDump.TaskType String The task type.
GravityZone.MemoryDump.Status String The status of the task started through gz-endpoint-create-memory-dump.
GravityZone.MemoryDump.EndpointID String The endpoint ID.
GravityZone.MemoryDump.Hostname String The endpoint hostname.
GravityZone.MemoryDump.StartDate Date The date and time when the memory dump task started (e.g., “2026-05-25T10:00:00Z”).
GravityZone.MemoryDump.EndDate Date The date and time when the memory dump task ended, if available (e.g., “2026-05-25T10:01:00Z”).
GravityZone.MemoryDump.ErrorCode String An error code if the task started through gz-endpoint-create-memory-dump fails, or “Success” if the task completes successfully.
GravityZone.MemoryDump.Error String An error message if the task started through gz-endpoint-create-memory-dump fails, or “Success” if the task completes successfully.
GravityZone.MemoryDump.DownloadURL String The memory dump download URL, available after the memory dump creation task completes successfully.

Command example

!gz-endpoint-memory-dump-status task_id=6836f4a01a2b3c4d5e6f7a8b endpoint_id=6942a43afe8d4e463ca5c197

Context Example

{
    "GravityZone": {
        "MemoryDump": {
            "TaskID": "6836f4a01a2b3c4d5e6f7a8b",
            "TaskType": "CreateMemoryDump",
            "Status": "Processed",
            "EndpointID": "6942a43afe8d4e463ca5c197",
            "Hostname": "bdvm",
            "StartDate": "2026-05-25T10:00:00Z",
            "EndDate": "2026-05-25T10:01:00Z",
            "ErrorCode": "Success",
            "Error": "Success",
            "DownloadURL": "https://example.com/memory-dump.zip"
        }
    }
}

Human Readable Output

This example illustrates the human-readable output when polling is set to true. When polling is false, only the status of the memory dump creation task is displayed.

Memory dump for endpoint 6942a43afe8d4e463ca5c197

EndpointID Hostname StartDate EndDate Error DownloadURL
6942a43afe8d4e463ca5c197 bdvm 2026-05-25T10:00:00Z 2026-05-25T10:01:00Z Success https://example.com/memory-dump.zip

gz-poll-endpoint-users-loggedin-status


Verifies the status of a task initiated via gz-endpoint-users-loggedin. This command is invoked automatically and is not intended for direct use.

Base Command

gz-poll-endpoint-users-loggedin-status

Input

Argument Name Description Required
task_id The ID of the task started through gz-endpoint-users-loggedin. Required
endpoint_id The ID of the managed endpoint associated with the specified task. Required

Context Output

There is no context output for this command.

Configuration parameters

  • url — Server URL (required)
  • credentials — (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetch — Fetch incidents
  • max_fetch — Maximum incidents to fetch
  • first_fetch — First fetch time
  • mirror_direction — Mirroring Direction
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval

Commands (27)

  • fetch-incidents

    Retrieves incidents from GravityZone Cloud.

  • get-mapping-fields

    Returns the list of fields available for outgoing mirroring. This command is intended for debugging purposes only.

  • get-modified-remote-data

    Retrieves incidents that were modified since the last data sync. This command is intended for debugging purposes.

  • get-remote-data

    Retrieves data from a remote incident without updating the current incident. This command is intended for debugging purposes.

  • gz-endpoint-create-memory-dump

    Initiates the creation of a memory dump for the specified endpoint. Once the dump is successfully generated, the resulting archive is uploaded to the Bitdefender cloud.

  • gz-endpoint-deisolate

    Restores an isolated endpoint to the network.

  • gz-endpoint-download-file

    Downloads a file from an endpoint.

  • gz-endpoint-download-investigation-package

    Collects and downloads an investigation package from an endpoint.

  • gz-endpoint-get

    Retrieves endpoint details by endpoint ID.

  • gz-endpoint-get-process-tree-by-hash

    Retrieves the process tree on an endpoint for a specified process hash. The applied policy must allow Live Search and the endpoint must be online.

  • gz-endpoint-isolate

    Isolates an endpoint from the network.

  • gz-endpoint-kill-process

    Terminates a process on an endpoint by process ID.

  • gz-endpoint-list

    Retrieves the list of managed endpoints.

  • gz-endpoint-list-by-running-process-hash

    Retrieves endpoints that are running processes with a specified hash. The API key and the applied policy must allow Live Search and endpoints must be online. The command waits up to five minutes for responses before timing out.

  • gz-endpoint-memory-dump-status

    Verifies the status of a task initiated via gz-endpoint-create-memory-dump.

  • gz-endpoint-run-command

    Runs a command on the endpoint. The applied policy must have remote shell enabled for this action to work.

  • gz-endpoint-upload-file

    Uploads a file to an endpoint.

  • gz-endpoint-users-loggedin

    Retrieves details of all active user sessions on a specific managed endpoint.

  • gz-incident-add-note

    Adds a note to one or more incidents.

  • gz-incident-change-status

    Changes the status of one or more incidents.

  • gz-incident-get

    Retrieves incident details by ID.

  • gz-incident-list

    Retrieves incidents from the last three days for all endpoints or for a specific endpoint.

  • gz-poll-endpoint-users-loggedin-status

    Verifies the status of a task initiated via gz-endpoint-users-loggedin. This command is invoked automatically and is not intended for direct use.

  • gz-poll-investigation-activity-status

    Checks the status of an investigation activity. This command is not intended for direct use.

  • gz-poll-live-search-status

    Checks the status of a Live Search query. This command is not intended for direct use.

  • gz-poll-task-status

    Checks the task status. This command is not intended for direct use.

  • update-remote-system

    Updates the remote incident with local incident changes without updating the current incident. This command is intended for debugging purposes.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401

"""IMPORTS"""

from typing import Any
from datetime import datetime, timedelta, UTC
import base64
import dateparser
import email
import io
import json
import tarfile
import urllib3
import uuid
import zipfile

# Disable insecure warnings

urllib3.disable_warnings()

""" CONSTANTS """


DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR
GZ_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S%z"  # GravityZone date format with timezone info
INTEGRATION_NAME = "GravityZone"
COMMAND_DOWNLOAD_FILE = "DownloadFile"
COMMAND_RUN_COMMAND = "RunCommand"
COMMAND_KILL_PROCESS = "KillProcess"
COMMAND_ISOLATE = "Isolate"
COMMAND_RESTORE_FROM_ISOLATION = "Deisolate"
COMMAND_UPLOAD_FILE = "UploadFile"
COMMAND_GET_ACTIVE_SESSIONS = "GetActiveSessions"
COMMAND_CREATE_MEMORY_DUMP = "CreateMemoryDump"

ACTIVITY_STATUS_SUCCESS = "success"
ACTIVITY_STATUS_PENDING = "pending"
ACTIVITY_STATUS_IN_PROGRESS = "in_progress"
ACTIVITY_STATUS_FAILED = "failed"
ACTIVITY_STATUS_TO_BE_RETRIEVED = "to_be_retrieved"

ACTIVITY_TYPE_DOWNLOAD_FILE = 1
ACTIVITY_TYPE_RUN_COMMAND = 2
ACTIVITY_NUMERIC_TO_COMMAND_NAME = {
    ACTIVITY_TYPE_DOWNLOAD_FILE: COMMAND_DOWNLOAD_FILE,
    ACTIVITY_TYPE_RUN_COMMAND: COMMAND_RUN_COMMAND,
}

LIVE_SEARCH_TIMEOUT = 600
LIVE_SEARCH_INTERVAL = 10
LIVE_SEARCH_QUERY_PROCESS_PER_HASH = "QUERY_PROCESS_PER_HASH"
LIVE_SEARCH_QUERY_RUNNING_HASH = "QUERY_RUNNING_HASH"

GET_INCIDENTS_DELTA_DAYS = 3
LOOPBACK_DAYS = 1

POLL_TIMEOUT = 1200
POLL_INTERVAL = 10
MEMORY_DUMP_POLL_TIMEOUT = 7200

FETCH_LIMIT = 50

TASK_TYPE_ISOLATE_ENDPOINT_PARENT = 16
TASK_TYPE_RESTORE_ENDPOINT_PARENT = 17
TASK_TYPE_KILL_PROCESS_PARENT = 21
TASK_TYPE_REMOTE_ACCESS_DOWNLOAD_PARENT = 24
TASK_TYPE_GET_ACTIVE_SESSIONS_PARENT = 26
TASK_TYPE_CREATE_MEMORY_DUMP_PARENT = 27
TASK_NUMERIC_TO_COMMAND_NAME = {
    TASK_TYPE_KILL_PROCESS_PARENT: COMMAND_KILL_PROCESS,
    TASK_TYPE_ISOLATE_ENDPOINT_PARENT: COMMAND_ISOLATE,
    TASK_TYPE_RESTORE_ENDPOINT_PARENT: COMMAND_RESTORE_FROM_ISOLATION,
    TASK_TYPE_REMOTE_ACCESS_DOWNLOAD_PARENT: COMMAND_UPLOAD_FILE,
    TASK_TYPE_GET_ACTIVE_SESSIONS_PARENT: COMMAND_GET_ACTIVE_SESSIONS,
    TASK_TYPE_CREATE_MEMORY_DUMP_PARENT: COMMAND_CREATE_MEMORY_DUMP,
}
TASK_STATUS_PENDING = 1
TASK_STATUS_PROCESSING = 2
TASK_STATUS_PROCESSED = 3
TASK_OUTPUT_HEADERS = ["EndpointID", "Hostname", "StartDate", "EndDate", "Error"]


INCIDENT_TYPE_EDR = "incident"
INCIDENT_TYPE_XDR = "extendedIncident"
INCIDENT_TYPE_MAPPING = {
    INCIDENT_TYPE_EDR: "Incident (EDR)",
    INCIDENT_TYPE_XDR: "Extended Incident (XDR)",
}
INCIDENT_STATUS_STR_OPEN = "open"
INCIDENT_STATUS_STR_CLOSED = "closed"
INCIDENT_STATUS_STR_IN_PROGRESS = "in_progress"
INCIDENT_STATUS_STR_FALSE_POSITIVE = "false_positive"

INCIDENT_STATUS_INT_MAPPING = {
    INCIDENT_STATUS_STR_OPEN: IncidentStatus.PENDING,
    INCIDENT_STATUS_STR_CLOSED: IncidentStatus.DONE,
    INCIDENT_STATUS_STR_IN_PROGRESS: IncidentStatus.ACTIVE,
    INCIDENT_STATUS_STR_FALSE_POSITIVE: IncidentStatus.ARCHIVE,
}
INCIDENT_STATUS_STR_MAPPING = {
    INCIDENT_STATUS_STR_OPEN: "Pending",
    INCIDENT_STATUS_STR_CLOSED: "Done",
    INCIDENT_STATUS_STR_IN_PROGRESS: "Active",
    INCIDENT_STATUS_STR_FALSE_POSITIVE: "Archived",
}

GRAVITY_ZONE_INCIDENT_STATUS_OPEN = 1
GRAVITY_ZONE_INCIDENT_STATUS_IN_PROGRESS = 2
GRAVITY_ZONE_INCIDENT_STATUS_DONE = 3
GRAVITY_ZONE_INCIDENT_STATUS_FALSE_POSITIVE = 4

INCIDENT_STATUS_MAPPING = {
    IncidentStatus.DONE: GRAVITY_ZONE_INCIDENT_STATUS_DONE,
    IncidentStatus.ARCHIVE: GRAVITY_ZONE_INCIDENT_STATUS_FALSE_POSITIVE,
    IncidentStatus.PENDING: GRAVITY_ZONE_INCIDENT_STATUS_OPEN,
    IncidentStatus.ACTIVE: GRAVITY_ZONE_INCIDENT_STATUS_IN_PROGRESS,
}

ENDPOINT_DEVICE_STATE_MAPPING = {1: "Online", 2: "Offline", 3: "Offline"}

ENDPOINTS_PER_PAGE = 100
LIVE_SEARCH_PER_PAGE = 100
INCIDENTS_PER_PAGE = 100

MEMORY_DUMP_STATUS_PROCESSED = "Processed"
MEMORY_DUMP_STATUS_PENDING = "Pending"

""" CLIENT CLASS """


class Client(BaseClient):
    def __init__(
        self,
        url: str,
        api_key: str | None = None,
        jwt_token: str | None = None,
        verify: bool = True,
        proxy: bool = False,
    ):
        """
        Client class to interact with GravityZone API.
        Args:
            url (str): Base URL of the GravityZone API.
            api_key (Optional[str]): API key for Basic Authentication.
            jwt_token (Optional[str]): JWT token for Bearer Authentication.
            verify (bool): Whether to verify SSL certificates.
            proxy (bool): Whether to use system proxy settings.
        Raises:
            DemistoException: If neither api_key nor jwt_token is provided.
        """
        super().__init__(base_url=url.strip("/"), verify=verify, proxy=proxy)

        if not api_key and (not jwt_token):
            raise DemistoException("Either 'api_key' or 'jwt_token' must be provided for authentication.")

        self.headers = self._build_headers(api_key, jwt_token)

    def _build_headers(self, api_key: str | None, jwt_token: str | None) -> dict[str, Any]:
        """
        Build the headers for the HTTP requests based on the authentication method.
        Args:
            api_key (Optional[str]): API key for Basic Authentication.
            jwt_token (Optional[str]): JWT token for Bearer Authentication.
        Returns:
            dict[str, Any]: Headers for the HTTP requests.
        """
        headers = {"Content-Type": "application/json"}
        if api_key:
            auth_sequence = f"{api_key}:".encode()
            headers["Authorization"] = f"Basic {base64.b64encode(auth_sequence).decode()}"
        elif jwt_token:
            headers["Authorization"] = f"Bearer {jwt_token}"
        return headers

    @logger
    def call(self, url_suffix: str, method: str, params: dict | None = None) -> Any:
        """
        Make a JSON-RPC call to the GravityZone API.
        Args:
            url_suffix (str): The URL suffix for the API endpoint.
            method (str): The JSON-RPC method to call.
            params (Optional[dict]): The parameters for the JSON-RPC method.
        Returns:
            Any: The result of the JSON-RPC call.
        Raises:
            DemistoException: If the JSON-RPC response contains an error.
        """
        url_suffix = url_suffix.lstrip("/")
        payload = {"jsonrpc": "2.0", "method": method, "params": params or {}, "id": 1}
        response = self._http_request(
            method="POST",
            url_suffix=f"/api/{url_suffix}",
            headers=self.headers,
            json_data=payload,
            params={},
            resp_type="json",
            return_empty_response=False,
            raise_on_status=True,
        )

        if "error" in response:
            error_code = response["error"].get("code")
            error_message = response["error"].get("message", "Unknown error")
            error_data = response["error"].get("data", {})
            raise DemistoException(f"JSON-RPC Error {error_code}: {error_message} - {error_data}")

        return response.get("result")

    @logger
    def upload_file(
        self,
        bucket_name: str,
        file_name: str,
        file_bytes: bytes,
        metadata: dict | None = None,
    ) -> Any:
        """
        Upload a file to the GravityZone storage bucket.
        Args:
            bucket_name (str): The name of the storage bucket.
            file_name (str): The name of the file to upload.
            file_bytes (bytes): The content of the file in bytes.
            metadata (Optional[dict]): Optional metadata for the file.
        Returns:
            Any: The response from the upload operation.
        """
        files = {"file": (file_name, file_bytes, "application/octet-stream")}
        data = {}
        if metadata:
            data["metadata"] = json.dumps(metadata)
        response = self._http_request(
            method="POST",
            url_suffix=f"/storage/{bucket_name}",
            headers={"Authorization": self.headers.get("Authorization", "")},
            files=files,
            data=data,
            params={},
            resp_type="json",
            return_empty_response=False,
            raise_on_status=True,
        )

        return response

    @logger
    def download_file(self, bucket_name: str, file_name: str) -> Any:
        """
        Download a file from the GravityZone storage bucket.
        Args:
            bucket_name (str): The name of the storage bucket.
            file_name (str): The name of the file to download.
        Returns:
            Any: The HTTP response containing the file content.
        """
        response = self._http_request(
            method="GET",
            url_suffix=f"/storage/{bucket_name}/{file_name}",
            headers=self.headers,
            params={},
            resp_type="response",
            return_empty_response=False,
            raise_on_status=True,
        )

        return response

    @logger
    def get_investigation_file_url(self, activity_id: str, target_id: str) -> Any:
        """
        Get the URL for an investigation file.
        Args:
            activity_id (str): The ID of the investigation activity.
            target_id (str): The ID of the target endpoint.
        Returns:
            Any: The response containing the file URL and status.
        """
        return self.call(
            "/v1.0/jsonrpc/investigation",
            "getInvestigationFileUrl",
            {"activityId": activity_id, "targetId": target_id},
        )

    @logger
    def get_task_status(self, task_id: str) -> Any:
        """
        Get the status of a task.
        Args:
            task_id (str): The ID of the task.
        Returns:
            Any: The response containing the task status.
        """
        return self.call(
            "/v1.1/jsonrpc/network",
            "getTaskStatus",
            {"taskId": task_id, "options": {"returnSubtasks": True}},
        )

    @logger
    def get_live_search_query_task_result(self, task_id: str, page: int = 1, per_page: int = LIVE_SEARCH_PER_PAGE) -> Any:
        """
        Get the results of a live search query task.
        Args:
            task_id (str): The ID of the live search query task.
            page (int): The page number to retrieve.
            per_page (int): The number of results per page.
        Returns:
            Any: The response containing the live search query task results.
        """
        return self.call(
            "/v1.2/jsonrpc/incidents",
            "getLiveSearchQueryTaskResult",
            {"taskId": task_id, "page": page, "perPage": per_page},
        )

    @logger
    def get_my_company(self) -> Any:
        """
        Get details of the authenticated user's company.
        Returns:
            Any: The response containing the company details.
        """
        return self.call("/v1.0/jsonrpc/companies", "getCompanyDetails")

    @logger
    def get_endpoints(self, limit: int, name: str | None = None, node_id: str | None = None) -> list[Any]:
        """
        Get the list of managed endpoints.
        Args:
            limit (int): The maximum number of endpoints to retrieve.
            name (Optional[str]): The naming pattern of the endpoints to retrieve.
            node_id (Optional[str]): The GravityZone company ID or group ID in the network tree.
        Returns:
            list[Any]: A list of managed endpoints.
        """
        if not node_id:
            company_details = self.get_my_company()
            node_id = company_details["id"]
        all_endpoints = []
        page = 1
        per_page = ENDPOINTS_PER_PAGE
        while True:
            response = self.call(
                "/v1.1/jsonrpc/network",
                "getNetworkInventoryItems",
                {
                    "parentId": node_id,
                    "page": page,
                    "perPage": per_page,
                    "filters": {
                        "type": {
                            "computers": True,
                            "virtualMachines": True,
                        },
                        "depth": {
                            "allItemsRecursively": True,
                        },
                        "security": {
                            "management": {
                                "managedWithBest": True,
                            },
                        },
                        **({"details": {"name": name}} if name else {}),
                    },
                },
            )

            items = response.get("items", [])
            all_endpoints.extend(items)
            if len(all_endpoints) >= limit or page >= response.get("pagesCount", 1) or not items:
                break
            page += 1
        return all_endpoints[:limit]

    @logger
    def get_endpoint(self, endpoint_id: str) -> Any:
        """
        Get details of a managed endpoint.
        Args:
            endpoint_id (str): The ID of the managed endpoint.
        Returns:
            Any: The response containing the endpoint details.
        """
        return self.call(
            "/v1.0/jsonrpc/network",
            "getManagedEndpointDetails",
            {
                "endpointId": endpoint_id,
                "options": {
                    "includeScanLogs": True,
                    "returnProductOutdated": True,
                    "includeLastLoggedUsers": True,
                },
            },
        )

    @logger
    def start_retrieve_investigation_file_from_endpoint(self, target_id: str, path: str) -> Any:
        """
        Start the retrieval of an investigation file from an endpoint.
        Args:
            target_id (str): The ID of the target endpoint.
            path (str): The path of the file to retrieve.
        Returns:
            Any: The response containing the activity ID.
        """
        return self.call(
            "/v1.0/jsonrpc/investigation",
            "startRetrieveInvestigationFileFromEndpoint",
            {"targetId": target_id, "path": path},
        )

    @logger
    def start_kill_process_on_endpoint(self, target_id: str, process_id: int) -> Any:
        """
        Start the process of killing a process on an endpoint.
        Args:
            target_id (str): The ID of the target endpoint.
            process_id (int): The ID of the process to kill.
        Returns:
            Any: The response containing the task ID.
        """
        return self.call(
            "/v1.0/jsonrpc/investigation",
            "killProcess",
            {"targetId": target_id, "processId": process_id},
        )

    @logger
    def start_command_execution_on_endpoint(self, target_id: str, command: str) -> Any:
        """
        Start the process of executing a command on an endpoint.
        Args:
            target_id (str): The ID of the target endpoint.
            command (str): The command to execute.
        Returns:
            Any: The response containing the activity ID.
        """
        return self.call(
            "/v1.0/jsonrpc/investigation",
            "startCommandExecutionOnEndpoint",
            {"targetId": target_id, "command": command},
        )

    @logger
    def start_isolate_endpoint(self, target_id: str) -> Any:
        """
        Start the process of isolating an endpoint.
        Args:
            target_id (str): The ID of the target endpoint.
        Returns:
            Any: The response containing the task ID.
        """
        return self.call(
            "/v1.1/jsonrpc/incidents",
            "createIsolateEndpointTask",
            {"endpointId": target_id},
        )

    @logger
    def start_deisolate_endpoint(self, target_id: str) -> Any:
        """
        Start the process of de-isolating an endpoint.
        Args:
            target_id (str): The ID of the target endpoint.
        Returns:
            Any: The response containing the task ID.
        """
        return self.call(
            "/v1.1/jsonrpc/incidents",
            "createRestoreEndpointFromIsolationTask",
            {"endpointId": target_id},
        )

    @logger
    def start_collect_investigation_package_on_endpoint(self, target_id: str) -> Any:
        """
        Start the process of collecting an investigation package on an endpoint.
        Args:
            target_id (str): The ID of the target endpoint.
        Returns:
            Any: The response containing the activity ID.
        """
        return self.call(
            "/v1.0/jsonrpc/investigation",
            "collectInvestigationPackage",
            {"targetId": target_id},
        )

    @logger
    def start_get_active_sessions_on_endpoint(self, endpoint_id: str) -> Any:
        """
        Start active session collection on one endpoint.
        Args:
            endpoint_id (str): The ID of the target endpoint.
        Returns:
            Any: The response containing the task ID.
        """
        return self.call(
            "/v1.0/jsonrpc/network",
            "createGetActiveSessionsTask",
            {"endpointId": endpoint_id},
        )

    @logger
    def start_create_memory_dump_on_endpoint(self, endpoint_id: str, path: str, password: str) -> Any:
        """
        Start memory dump creation on one endpoint.
        Args:
            endpoint_id (str): The ID of the target endpoint.
            path (str): The folder path on endpoint where the dump archive is generated.
            password (str): Password used to protect the resulting dump archive.
        Returns:
            Any: The response containing the memory dump task ID.
        """
        return self.call(
            "/v1.0/jsonrpc/network",
            "createMemoryDumpTask",
            {"endpointId": endpoint_id, "path": path, "password": password},
        )

    @logger
    def start_live_search_query_find_running_process_tree_by_hash(self, endpoints: list[str], process_hash: str) -> Any:
        """
        Start a live search query to find running process trees by hash.
        Args:
            endpoints (List[str]): List of endpoint IDs to query.
            process_hash (str): The hash of the process to search for.
        Returns:
            Any: The response containing the live search query information.
        """
        return self.call(
            "/v1.2/jsonrpc/incidents",
            "runPredefinedLiveSearchQuery",
            {
                "endpoints": endpoints,
                "queryType": LIVE_SEARCH_QUERY_PROCESS_PER_HASH,
                "querySpecifics": {"hash": process_hash},
            },
        )

    @logger
    def start_live_search_query_find_endpoints_running_process_by_hash(self, endpoints: list[str], process_hash: str) -> Any:
        """
        Start a live search query to find endpoints running a specific process by hash.
        Args:
            endpoints (List[str]): List of endpoint IDs to query.
            process_hash (str): The hash of the process to search for.
        Returns:
            Any: The response containing the live search query information.
        """
        return self.call(
            "/v1.2/jsonrpc/incidents",
            "runPredefinedLiveSearchQuery",
            {
                "endpoints": endpoints,
                "queryType": LIVE_SEARCH_QUERY_RUNNING_HASH,
                "querySpecifics": {"hash": process_hash},
            },
        )

    @logger
    def get_incidents(
        self,
        start_time: str | None = None,
        end_time: str | None = None,
        target_id: str | None = None,
        max_fetch: int = FETCH_LIMIT,
    ) -> list[dict[str, Any]]:
        """
        Get a list of incidents with optional filtering by time range and target ID.
        Args:
            start_time (Optional[str]): The start time for filtering incidents.
            end_time (Optional[str]): The end time for filtering incidents.
            target_id (Optional[str]): The target endpoint ID for filtering incidents.
            max_fetch (int): The maximum number of incidents to fetch.
        Returns:
            List[dict[str, Any]]: A list of incidents.
        """
        filters: dict[str, Any] = {}
        if start_time and end_time:
            filters.update({"startDate": start_time, "endDate": end_time})
        if target_id:
            filters.update({"endpointId": target_id})
        params: dict[str, Any] = {
            "filters": filters,
            "options": {"includeChildCompanies": True},
            "page": 1,
            "perPage": INCIDENTS_PER_PAGE,
        }

        page = 1
        all_incidents = []
        while True:
            params["page"] = page
            response = self.call("/v1.2/jsonrpc/incidents", "getIncidentsList", params)
            items = response.get("items", [])
            all_incidents.extend(items)
            if len(all_incidents) >= max_fetch:
                break
            if page >= response.get("pagesCount", 1) or not items:
                break
            page += 1
        return all_incidents[:max_fetch]

    @logger
    def get_incident(self, incident_id: str) -> dict[str, Any]:
        """
        Get details of a specific incident by its ID.
        Args:
            incident_id (str): The ID of the incident.
        Returns:
            dict[str, Any]: The response containing the incident details.
        """
        return self.call("/v1.2/jsonrpc/incidents", "getIncident", {"id": incident_id})

    @logger
    def add_incident_note(self, incident_type: str, incident_id: str, note: str) -> dict[str, Any]:
        """
        Add a note to a specific incident.
        Args:
            incident_type (str): The type of the incident.
            incident_id (str): The ID of the incident.
            note (str): The note to add.
        Returns:
            dict[str, Any]: The response from the add operation.
        """
        type = f"{incident_type}s"
        return self.call(
            "/v1.0/jsonrpc/incidents",
            "updateIncidentNote",
            {"type": type, "incidentId": incident_id, "note": note},
        )

    @logger
    def change_incident_status(self, incident_id: str, status: int) -> dict[str, Any]:
        """
        Change the status of a specific incident.
        Args:
            incident_id (str): The ID of the incident.
            status (int): The new status for the incident.
        Returns:
            dict[str, Any]: The response from the status change operation.
        """
        return self.call(
            "/v1.0/jsonrpc/incidents",
            "changeIncidentStatus",
            {"type": "incidents", "incidentId": incident_id, "status": status},
        )


""" HELPER FUNCTIONS """


class FileManagement:
    def __init__(self, client: Client) -> None:
        self.client = client

    @logger
    def get_file(self, entry_id: str) -> tuple[str, bytes]:
        """
        Get the file name and content for a given Demisto entry ID.
        Args:
            entry_id (str): The Demisto entry ID of the file.
        Returns:
            tuple[str, bytes]: The file name and its content as bytes.
        """
        file_ = demisto.getFilePath(entry_id)

        file_name = file_.get("name")
        file_path = file_.get("path")

        if not file_path:
            raise DemistoException(f"Could not find file for entry ID {entry_id}")
        if not file_name:
            raise DemistoException(f"Could not determine the file name for entry ID {entry_id}")

        with open(file_path, "rb") as fopen:
            file_bytes = fopen.read()

        return file_name, file_bytes

    @logger
    def upload_file(self, bucket_name: str, entry_id: str, metadata: dict | None = None) -> dict[str, Any]:
        """
        Upload a file to the GravityZone storage bucket from a Demisto entry ID.
        Args:
            bucket_name (str): The name of the storage bucket.
            entry_id (str): The Demisto entry ID of the file to upload.
            metadata (Optional[dict]): Optional metadata for the file.
        Returns:
            dict[str, Any]: The response from the upload operation.
        """
        file_name, file_bytes = self.get_file(entry_id)
        try:
            return self.client.upload_file(bucket_name, file_name, file_bytes, metadata)
        except FileNotFoundError:
            raise DemistoException(f"File not found: {file_name} for entry ID {entry_id}")

    @logger
    def _read_zip_from_memory(self, zip_bytes: bytes) -> bytes | None:
        """
        Read the first file from a ZIP archive in memory.
        Args:
            zip_bytes (bytes): The bytes of the ZIP archive.
        Returns:
            Optional[bytes]: The content of the first file in the archive, or None if no file is found.
        """
        with zipfile.ZipFile(io.BytesIO(zip_bytes), "r") as zip_ref:
            for file_name in zip_ref.namelist():
                if not file_name.endswith("/"):
                    with zip_ref.open(file_name) as file:
                        return file.read()
        return None

    @logger
    def _read_tgz_from_memory(self, tgz_bytes: bytes) -> bytes | None:
        """
        Read the first file from a TGZ archive in memory.
        Args:
            tgz_bytes (bytes): The bytes of the TGZ archive.
        Returns:
            Optional[bytes]: The content of the first file in the archive, or None if no file is found.
        """
        with tarfile.open(fileobj=io.BytesIO(tgz_bytes), mode="r:gz") as tar:
            for member in tar.getmembers():
                if member.isfile():
                    extracted_file = tar.extractfile(member)
                    return extracted_file.read() if extracted_file else None
        return None

    @logger
    def _get_file_content_from_archive(self, archive_bytes: bytes) -> str | None:
        """
        Extract the content of the first file from an archive in memory.
        Args:
            archive_bytes (bytes): The bytes of the archive.
        Returns:
            Optional[str]: The content of the first file in the archive as a string, or None if no file is found.
        """
        if archive_bytes[:2] == b"PK":
            content = self._read_zip_from_memory(archive_bytes)
        elif archive_bytes[:2] == b"7z":
            content = None
        elif archive_bytes[:2] == b"\x1f\x8b":
            content = self._read_tgz_from_memory(archive_bytes)
        else:
            content = archive_bytes

        if content:
            return content.decode()
        else:
            return None

    @logger
    def download_file(self, bucket_name: str, file_name: str, save_path: str | None = None) -> Union[dict[str, Any], str, None]:
        """
        Download a file from the GravityZone storage bucket.
        Args:
            bucket_name (str): The name of the storage bucket.
            file_name (str): The name of the file to download.
            save_path (Optional[str]): The local path to save the file. If None, returns file content.
        Returns:
            Union[dict[str, Any], str, None]: The file result if saved locally, the file content as a string, or None on error.
        """
        try:
            response = self.client.download_file(bucket_name, file_name)
            if save_path:
                content_disposition = response.headers.get("Content-Disposition", "").lower()
                if content_disposition:
                    filename = email.message_from_string(f"Content-Disposition: {content_disposition}\n\n").get_filename()
                    if filename:
                        result = fileResult(save_path, response.content)
                        return result
                return None
            else:
                unpacked_archive = self._get_file_content_from_archive(response.content)
                if unpacked_archive is not None:
                    return unpacked_archive
                else:
                    unique_filename = f"command_output_{uuid.uuid4().hex}.7z"
                    return fileResult(unique_filename, response.content)
        except Exception:
            return None


@polling_function(
    name="gz-poll-investigation-activity-status",
    timeout=POLL_TIMEOUT,
    interval=POLL_INTERVAL,
    requires_polling_arg=False,
)
def check_investigation_status(args: dict[str, Any], client: Client) -> PollResult:
    """
    Polling function to check the status of an investigation activity.
    Args:
        args (Dict[str, Any]): The arguments containing target_id, activity_id, output_file, and metadata.
            target_id (str): The ID of the target endpoint.
            activity_id (str): The ID of the investigation activity.
            output_file (str): The local path to save the file.
            metadata (Dict[str, Any]): Additional metadata for the activity.
        client (Client): The GravityZone client instance.
    Returns:
        PollResult: The result of the polling operation.
    """
    target_id = args.get("target_id")
    activity_id = args.get("activity_id")
    output_file = args.get("output_file")
    metadata = args.get("metadata", {})
    if isinstance(metadata, str):
        metadata = json.loads(metadata)
    if not output_file:
        output_file = None

    if not target_id or not activity_id:
        return PollResult(
            CommandResults(
                readable_output="Both 'target_id' and 'activity_id' must be provided.",
                entry_type=EntryType.ERROR,
            )
        )

    investigation_file_url = client.get_investigation_file_url(activity_id, target_id)

    current_status = investigation_file_url.get("status")
    url = investigation_file_url.get("url")
    error_code = investigation_file_url.get("errorCode")

    activity_type = metadata.get("activityType", 0)

    command_name_from_activity_type = ACTIVITY_NUMERIC_TO_COMMAND_NAME.get(activity_type, f"UnknownActivity_{hex(activity_type)}")

    activity_output_prefix = f"GravityZone.Command.{command_name_from_activity_type}"

    headers = ["EndpointID", "Status", "ErrorCode"]

    raw_response = {
        "activity_id": activity_id,
        "activity_type": command_name_from_activity_type,
        "target_id": target_id,
        "status": current_status,
        "url": url,
        "errorCode": error_code,
    }

    data = {
        "ActivityID": activity_id,
        "ActivityType": command_name_from_activity_type,
        "Status": "Pending",
        "EndpointID": target_id,
        "ErrorCode": error_code,
    }

    if activity_type == ACTIVITY_TYPE_RUN_COMMAND:
        data["Command"] = metadata.get("command")
        headers.extend(["Command"])
    elif activity_type == ACTIVITY_TYPE_DOWNLOAD_FILE:
        data["RemoteFile"] = metadata.get("remoteFile")
        data["OutputFile"] = metadata.get("outputFile")
        headers.extend(["OutputFile", "RemoteFile"])

    if current_status == ACTIVITY_STATUS_SUCCESS:
        bucket = investigation_file_url.get("bucket")
        if not bucket:
            data["Status"] = "Failed"
            data["ErrorCode"] = "NoBucket"
            return PollResult(
                CommandResults(
                    raw_response=raw_response,
                    readable_output=tableToMarkdown(
                        f"{activity_output_prefix} command on host {target_id}:",
                        [data],
                        headers=headers,
                    ),
                    outputs=[data],
                    outputs_prefix=activity_output_prefix,
                    outputs_key_field="ActivityID",
                    entry_type=EntryType.ERROR,
                )
            )

        file_management = FileManagement(client)
        result = file_management.download_file(bucket, investigation_file_url["fileId"], output_file)

        if isinstance(result, str):
            data["Output"] = result
            headers.append("Output")
        elif isinstance(result, dict):
            data["FileID"] = result.get("FileID", "")
            data["FileName"] = result.get("File", "")
            headers.append("FileID")
        elif result is None:
            data["Status"] = "Failed"
            data["ErrorCode"] = "DownloadFailed"
            return PollResult(
                CommandResults(
                    raw_response=raw_response,
                    readable_output=tableToMarkdown(
                        f"{activity_output_prefix} command on host {target_id}:",
                        [data],
                        headers=headers,
                    ),
                    outputs=[data],
                    outputs_prefix=activity_output_prefix,
                    outputs_key_field="ActivityID",
                    entry_type=EntryType.ERROR,
                )
            )

        data["Status"] = "Success"

        return PollResult(
            response=[
                CommandResults(
                    raw_response=result,
                    readable_output=tableToMarkdown(
                        f"{activity_output_prefix} command on host {target_id}:",
                        [data],
                        headers=headers,
                    ),
                    outputs=[data],
                    outputs_prefix=activity_output_prefix,
                    outputs_key_field="ActivityID",
                    entry_type=EntryType.NOTE,
                ),
                result,
            ]
        )

    if current_status == ACTIVITY_STATUS_FAILED:
        data["Status"] = "Failed"
        return PollResult(
            CommandResults(
                raw_response=raw_response,
                readable_output=tableToMarkdown(
                    f"{activity_output_prefix} command on host {target_id}:",
                    [data],
                    headers=headers,
                ),
                outputs=[data],
                outputs_prefix=activity_output_prefix,
                outputs_key_field="ActivityID",
                entry_type=EntryType.ERROR,
            )
        )

    return PollResult(
        continue_to_poll=True,
        response=CommandResults(
            raw_response=raw_response,
            readable_output=f"Activity {command_name_from_activity_type} ('{activity_id}') still in progress, host {target_id}.",
            entry_type=EntryType.NOTE,
        ),
    )


@logger
def get_investigation_results(
    client: Client,
    target_id: str,
    activity_id: str,
    output_file: str,
    metadata: dict[str, Any],
) -> PollResult:
    """
    Get the results of an investigation activity.
    Args:
        client (Client): The GravityZone client instance.
        target_id (str): The ID of the target endpoint.
        activity_id (str): The ID of the investigation activity.
        output_file (str): The local path to save the file.
        metadata (Dict[str, Any]): Additional metadata for the activity.
    Returns:
        PollResult: The result of the investigation activity.
    """
    return check_investigation_status(
        {
            "target_id": target_id,
            "activity_id": activity_id,
            "output_file": output_file,
            "metadata": metadata,
        },
        client,
    )


@polling_function(
    name="gz-poll-task-status",
    timeout=POLL_TIMEOUT,
    interval=POLL_INTERVAL,
    requires_polling_arg=False,
)
def check_task_status(
    args: dict[str, Any],
    client: Client,
) -> PollResult:
    """
    Polling function to check the status of a task.
    Args:
        args (Dict[str, Any]): The arguments containing task_id and metadata.
            task_id (str): The ID of the task.
            metadata (Dict[str, Any]): Additional metadata for the task.
        client (Client): The GravityZone client instance.
    Returns:
        PollResult: The result of the polling operation.
    """
    task_id = args.get("task_id", "unknown")
    metadata = args.get("metadata", {})
    if isinstance(metadata, str):
        metadata = json.loads(metadata)

    task_output = client.get_task_status(task_id)

    current_status = task_output.get("status")

    if current_status == TASK_STATUS_PROCESSED:
        return PollResult(
            generate_processed_task_command_result(
                task_id,
                task_output,
                metadata,
                error_code=next(
                    (subtask["errorCode"] for subtask in (task_output.get("subtasks") or []) if subtask.get("status") == 2),
                    None,
                ),
            )
        )

    return PollResult(
        continue_to_poll=True,
        response=CommandResults(
            raw_response=task_output,
            readable_output=f"Task '{task_id}' still pending.",
            entry_type=EntryType.NOTE,
        ),
    )


@logger
def get_task_results(
    client: Client,
    task_id: str,
    metadata: dict[str, Any],
) -> PollResult:
    """
    Get the results of a task.
    Args:
        client (Client): The GravityZone client instance.
        task_id (str): The ID of the task.
        metadata (Dict[str, Any]): Additional metadata for the task.
    Returns:
        PollResult: The result of the task.
    """
    return check_task_status({"task_id": task_id, "metadata": metadata}, client)


@polling_function(
    name="gz-poll-live-search-status",
    timeout=LIVE_SEARCH_TIMEOUT,
    interval=LIVE_SEARCH_INTERVAL,
    requires_polling_arg=False,
)
def query_live_search_results(args: dict[str, Any], client: Client) -> PollResult:
    """
    Polling function to query live search results.
    Args:
        args (Dict[str, Any]): The arguments containing task_id.
            task_id (str): The ID of the live search query task.
            search_type (str): The type of live search query.
        client (Client): The GravityZone client instance.
    Returns:
        PollResult: The result of the live search query.
    """
    task_id = args.get("task_id")
    search_type = args.get("search_type", "")
    if search_type == LIVE_SEARCH_QUERY_PROCESS_PER_HASH:
        output_prefix = "GravityZone.Command.ProcessTreeForHash"
        outputs_key_field = "EndpointID"
    elif search_type == LIVE_SEARCH_QUERY_RUNNING_HASH:
        output_prefix = "GravityZone.Command.EndpointsRunningProcessHash"
        outputs_key_field = "ProcessHash"
    else:
        output_prefix = "GravityZone.Command.UnknownLiveSearchType"
        outputs_key_field = "EndpointID"
    metadata = args.get("metadata", "{}")
    if isinstance(metadata, str):
        metadata = json.loads(metadata)

    if not task_id:
        return PollResult(
            CommandResults(
                readable_output="`task_id` must be provided.",
                entry_type=EntryType.ERROR,
            )
        )

    try:
        response = client.get_live_search_query_task_result(task_id, page=1, per_page=5000)
        pages_count = int(response["pagesCount"]) + 1
        results = response["items"]

        for i in range(2, pages_count):
            results.extend(client.get_live_search_query_task_result(task_id, page=i, per_page=5000)["items"])

        return PollResult(
            CommandResults(
                raw_response=results,
                readable_output=tableToMarkdown(
                    "Live Search Results",
                    [generate_human_readable_live_search_result(x, metadata) for x in results],
                ),
                outputs=[generate_context_live_search_result(x, metadata) for x in results],
                outputs_prefix=output_prefix,
                outputs_key_field=outputs_key_field,
            )
        )

    except Exception as e:
        return PollResult(
            continue_to_poll=True,
            response=CommandResults(
                raw_response=str(e),
                readable_output=f"Query {task_id} still pending. Got error: {str(e)}",
                entry_type=EntryType.ERROR,
            ),
        )


@logger
def get_live_search_results(client: Client, task_id: str, search_type: str, metadata: str = "{}") -> PollResult:
    """
    Get the results of a live search query.
    Args:
        client (Client): The GravityZone client instance.
        task_id (str): The ID of the live search query task.
    Returns:
        PollResult: The result of the live search query.
    """
    return query_live_search_results({"task_id": task_id, "search_type": search_type, "metadata": metadata}, client)


def generate_context_for_incident(raw_incident: dict[str, Any]) -> dict[str, Any]:
    """
    Generate the context dictionary for a GravityZone incident.
    Args:
        raw_incident (Dict[str, Any]): The raw incident data from GravityZone.
    Returns:
        Dict[str, Any]: The formatted incident context.
    """
    data = {
        "ID": raw_incident.get("incidentId"),
        "Number": raw_incident.get("incidentNumber"),
        "Type": raw_incident.get("incidentType"),
        "Company": {
            "Name": raw_incident.get("company", {}).get("name"),
            "ID": raw_incident.get("company", {}).get("id"),
        },
        "Severity": {"Score": raw_incident.get("severityScore")},
        "Status": INCIDENT_STATUS_INT_MAPPING.get(raw_incident.get("status", "open"), IncidentStatus.PENDING),
        "ActionTaken": raw_incident.get("mainAction"),
        "Created": raw_incident.get("created"),
        "LastUpdated": raw_incident.get("lastUpdated"),
        "LastProcessed": raw_incident.get("lastProcessed"),
        "Permalink": raw_incident.get("incidentLink"),
        "AssignedPriority": raw_incident.get("priority"),
        "AssignedUser": format_incident_assignee_for_context(raw_incident),
        "Notes": format_incident_notes_for_context(raw_incident),
        "Alerts": format_incident_alerts_for_context(raw_incident),
        "RawJSON": raw_incident,
    }
    computer_id = raw_incident.get("details", {}).get("computerId")
    computer_name = raw_incident.get("details", {}).get("computerName")
    computer_ip = raw_incident.get("details", {}).get("computerIp")
    if computer_id:
        data["EndpointID"] = computer_id
    if computer_name:
        data["EndpointHostname"] = computer_name
    if computer_ip:
        data["EndpointIP"] = computer_ip
    return data


def format_incident_assignee_for_context(
    raw_incident: dict[str, Any],
) -> dict[str, Any] | None:
    """
    Format the assignee information for the incident context.
    Args:
        raw_incident (Dict[str, Any]): The raw incident data from GravityZone.
    Returns:
        Dict[str, Any] | None: The formatted assignee information or None if no assignee.
    """
    assignee = raw_incident.get("assignee")
    if not assignee:
        return None
    return {
        "ID": assignee.get("userId"),
        "Email": assignee.get("userName"),
        "Company": {
            "ID": assignee.get("companyId"),
            "Name": assignee.get("companyName"),
        },
    }


def format_incident_notes_for_context(
    raw_incident: dict[str, Any],
) -> list[dict[str, str]]:
    """
    Format the notes for the incident context.
    Args:
        raw_incident (Dict[str, Any]): The raw incident data from GravityZone.
    Returns:
        List[Dict[str, str]]: The formatted list of notes.
    """
    notes = []
    for note in raw_incident.get("notes", []):
        notes.append(
            {
                "Text": note.get("text"),
                "User": note.get("userName"),
                "Date": note.get("created"),
            }
        )
    return notes


def format_incident_alerts_for_context(
    raw_incident: dict[str, Any],
) -> list[dict[str, str]]:
    """
    Format the alerts for the incident context.
    Args:
        raw_incident (Dict[str, Any]): The raw incident data from GravityZone.
    Returns:
        List[Dict[str, str]]: The formatted list of alerts.
    """
    alerts = []
    incident_type = raw_incident.get("incidentType")
    if incident_type == INCIDENT_TYPE_EDR:
        for item in raw_incident.get("details", {}).get("alerts"):
            alerts.append(format_edr_alert_for_context(item, raw_incident))
    elif incident_type == INCIDENT_TYPE_XDR:
        for item in raw_incident.get("details", {}).get("alerts"):
            alerts.append(format_xdr_alert_for_context(item, raw_incident))
    return alerts


def format_edr_alert_for_context(alert: dict[str, Any], raw_incident: dict[str, Any]) -> dict[str, Any]:
    """
    Format an EDR alert for the incident context.
    Args:
        alert (Dict[str, Any]): The raw alert data from GravityZone.
        raw_incident (Dict[str, Any]): The raw incident data from GravityZone.
    Returns:
        Dict[str, Any]: The formatted alert context.
    """
    result: dict[str, Any] = {
        "Name": alert.get("name"),
        "Date": alert.get("date"),
        "DetectedBy": {
            "Name": alert.get("detectedBy", {}).get("name"),
            "Class": alert.get("detectedBy", {}).get("class"),
        },
        "Resources": [],
    }
    alert_resources = alert.get("resources", [])
    for resource in alert_resources:
        context_resource = {}
        for key, value in resource.get("details", {}).items():
            if value is not None:
                context_resource[key[:1].upper() + key[1:]] = value
        resource_type = resource.get("type", "unknown")
        context_resource["Type"] = resource_type
        result["Resources"].append(context_resource)
    return result


def format_xdr_alert_for_context(alert: dict[str, Any], raw_incident: dict[str, Any]) -> dict[str, str]:
    """
    Format an XDR alert for the incident context.
    Args:
        alert (Dict[str, Any]): The raw alert data from GravityZone.
        raw_incident (Dict[str, Any]): The raw incident data from GravityZone.
    Returns:
        Dict[str, str]: The formatted alert context.
    """
    result = {
        "Name": alert.get("name"),
        "Date": alert.get("date"),
        "Sensors": alert.get("sensors", []),
        "Tactic": alert.get("tactic"),
        "Transitions": [],
    }
    transitions = alert.get("transitions", [])
    if not transitions:
        return result
    for transition in transitions:
        to_node_id = transition.get("to", "")
        from_node_id = transition.get("from", "")
        to_node = get_node_by_id(to_node_id, raw_incident)
        from_node = get_node_by_id(from_node_id, raw_incident)
        to_node_name = to_node.get("name") if to_node else "[undefined]"
        from_node_name = from_node.get("name") if from_node else "[undefined]"

        resources = transition.get("resources", [])
        transition_resources = []
        for resource in resources:
            context_resource = {}
            for key, value in resource.get("details", {}).items():
                if value is not None:
                    context_resource[key[:1].upper() + key[1:]] = value
            resource_type = resource.get("type", "unknown")
            context_resource["Type"] = resource_type
            transition_resources.append(context_resource)

        result["Transitions"].append(
            {
                "From": {"NodeID": from_node_id, "NodeName": from_node_name},
                "To": {"NodeID": to_node_id, "NodeName": to_node_name},
                "Resources": transition_resources,
            }
        )
    return result


def generate_context_for_summarized_incidents(
    raw_incidents: list[dict[str, Any]],
) -> list[dict[str, str | None]]:
    """
    Generate the context list for summarized GravityZone incidents.
    Args:
        raw_incidents (List[Dict[str, Any]]): The list of raw incident data from GravityZone.
    Returns:
        List[Dict[str, str | None]]: The list of formatted incident contexts.
    """
    incidents = []
    for raw_incident in raw_incidents:
        incident = {
            "ID": raw_incident.get("incidentId"),
            "Number": raw_incident.get("incidentNumber"),
            "Type": raw_incident.get("incidentType"),
            "CompanyName": raw_incident.get("company", {}).get("name"),
            "CompanyID": raw_incident.get("company", {}).get("id"),
            "SeverityScore": raw_incident.get("severityScore"),
            "Status": INCIDENT_STATUS_INT_MAPPING.get(raw_incident.get("status", "open"), IncidentStatus.PENDING),
            "ActionTaken": raw_incident.get("mainAction"),
            "Created": raw_incident.get("created"),
            "LastUpdated": raw_incident.get("lastUpdated"),
            "LastProcessed": raw_incident.get("lastProcessed"),
            "Permalink": raw_incident.get("incidentLink"),
            "AssignedPriority": raw_incident.get("priority"),
            "AssignedUserId": raw_incident.get("assignee"),
            "AttackTypes": raw_incident.get("attackTypes"),
            "RawJSON": raw_incident,
        }
        computer_id = raw_incident.get("details", {}).get("computerId")
        computer_name = raw_incident.get("details", {}).get("computerName")
        computer_ip = raw_incident.get("details", {}).get("computerIp")
        if computer_id:
            incident["EndpointID"] = computer_id
        if computer_name:
            incident["EndpointHostname"] = computer_name
        if computer_ip:
            incident["EndpointIP"] = computer_ip
        incidents.append(incident)
    return incidents


def generate_endpoint_by_contex_standard(device) -> Common.Endpoint:
    """
    Generate an endpoint object from device data using the context standard.
    Args:
        device (dict): The device data from GravityZone.
    Returns:
        Common.Endpoint: The generated endpoint object.
    """
    device_id = device.get("id")
    device_state = device.get("state")
    state = ENDPOINT_DEVICE_STATE_MAPPING.get(device_state, "Offline")
    endpoint = Common.Endpoint(
        id=device_id,
        hostname=device.get("name"),
        ip_address=device.get("ip"),
        os=device.get("operatingSystem"),
        status=state,
        vendor=INTEGRATION_NAME,
    )
    return endpoint


def generate_endpoint_entry_from_list(device) -> dict[str, Any]:
    """
    Generate an endpoint entry dictionary from device data obtained from the list of endpoints.
    Args:
        device (dict): The device data from GravityZone.
    Returns:
        Dict[str, Any]: The generated endpoint entry.
    """
    device_id = device.get("id")
    device_details = device.get("details", {}) or {}
    entry = {
        "ID": device_id,
        "CompanyID": device.get("companyId"),
        "GroupID": device_details.get("groupId"),
        "Hostname": device.get("name"),
        "FQDN": device_details.get("fqdn"),
        "IP": device_details.get("ip"),
        "OS": device_details.get("operatingSystemVersion"),
        "MAC": (device_details.get("macs") or [None])[0],
        "Vendor": INTEGRATION_NAME,
    }
    return entry


def generate_endpoint_entry(device) -> dict[str, Any]:
    """
    Generate an endpoint entry dictionary from device data.
    Args:
        device (dict): The device data from GravityZone.
    Returns:
        Dict[str, Any]: The generated endpoint entry.
    """
    device_id = device.get("id")
    entry = {
        "ID": device_id,
        "Hostname": device.get("name"),
        "IP": device.get("ip"),
        "OS": device.get("operatingSystem"),
        "Status": ENDPOINT_DEVICE_STATE_MAPPING.get(device.get("state"), "Offline"),
        "Vendor": INTEGRATION_NAME,
        "LastLoggedUsers": ", ".join(device.get("lastLoggedUsers", [])),
    }
    return entry


def generate_endpoint_from_list_by_contex_standard(device) -> Common.Endpoint:
    """
    Generate an endpoint object from device data in a list using the context standard.
    Args:
        device (dict): The device data from GravityZone.
    Returns:
        Common.Endpoint: The generated endpoint object.
    """
    device_id = device.get("id")
    endpoint = Common.Endpoint(
        id=device_id,
        hostname=device.get("name"),
        ip_address=device.get("ip"),
        os=device.get("operatingSystemVersion"),
        mac_address=device.get("macs")[0],
        vendor=INTEGRATION_NAME,
    )
    return endpoint


def generate_human_readable_incident_from_context(
    context_incident: dict[str, Any],
) -> str:
    """
    Generate a human-readable string for a GravityZone incident from its context.
    Args:
        context_incident (Dict[str, Any]): The incident context data.
    Returns:
        str: The human-readable incident string.
    """
    incident_readable_output = get_incident_human_readable_output(context_incident)
    notes_readable_output = get_incident_notes_human_readable_output(context_incident)
    alerts_readable_output = get_incident_alerts_human_readable_output(context_incident)
    return f"{incident_readable_output}\n{notes_readable_output}\n{alerts_readable_output}"


def get_incident_human_readable_output(context_incident: dict[str, Any]) -> str:
    """
    Generate the human-readable output for a GravityZone incident.
    Args:
        context_incident (Dict[str, Any]): The incident context data.
    Returns:
        str: The human-readable incident string."""
    data = {
        "ID": context_incident.get("ID"),
        "Type": INCIDENT_TYPE_MAPPING.get(context_incident.get("Type", "unknown"), "Unknown"),
        "Number": context_incident.get("Number"),
        "Company Name": context_incident.get("Company", {}).get("Name"),
        "Severity Score": str(context_incident.get("Severity", {}).get("Score")) + "%",
        "Status": str(context_incident.get("Status"))
        + " ("
        + INCIDENT_STATUS_STR_MAPPING.get(context_incident.get("RawJSON", {}).get("status", ""), "unknown")
        + ")",
        "Action Taken": context_incident.get("ActionTaken", "").capitalize(),
        "Created": context_incident.get("Created"),
        "Last Updated": context_incident.get("LastUpdated"),
        "Last Processed": context_incident.get("LastProcessed"),
        "Permalink": context_incident.get("Permalink"),
        "Assigned Priority": context_incident.get("AssignedPriority", "").capitalize(),
    }
    assignee = context_incident.get("AssignedUser")
    if not assignee:
        data["Assigned User"] = "Unassigned"
    else:
        data["Assigned User"] = assignee.get("Email") + " (" + assignee.get("ID") + ")"
    computer_id = context_incident.get("EndpointID")
    computer_name = context_incident.get("EndpointHostname")
    computer_ip = context_incident.get("EndpointIP")
    if computer_id:
        data["Endpoint ID"] = computer_id
    if computer_name:
        data["Endpoint Name"] = computer_name
    if computer_ip:
        data["Endpoint IP"] = computer_ip
    return tableToMarkdown("GravityZone Incident", data)


def get_incident_notes_human_readable_output(context_incident: dict[str, Any]) -> str:
    """
    Generate the human-readable output for the notes of a GravityZone incident.
    Args:
        context_incident (Dict[str, Any]): The incident context data.
    Returns:
        str: The human-readable notes string.
    """
    data = []
    for note in context_incident.get("Notes") or []:
        data.append(
            {
                "Text": note.get("Text"),
                "User": note.get("User"),
                "Date": note.get("Date"),
            }
        )
    return tableToMarkdown("Incident Notes", data, headers=["Text", "User", "Date"])


def get_incident_alerts_human_readable_output(context_incident: dict[str, Any]) -> str:
    """
    Generate the human-readable output for the alerts of a GravityZone incident.
    Args:
        context_incident (Dict[str, Any]): The incident context data.
    Returns:
        str: The human-readable alerts string.
    """
    data = []
    for alert in context_incident.get("Alerts", []):
        row = {
            "Name": alert.get("Name"),
            "Date": alert.get("Date"),
        }
        sensors = alert.get("Sensors")
        tactic = alert.get("Tactic")
        detected_by = alert.get("DetectedBy")
        transitions = alert.get("Transitions")
        resources = alert.get("Resources")
        if sensors:
            row["Sensors"] = ", ".join(sensors)
        if tactic:
            row["Tactic"] = tactic
        if detected_by:
            row["Detected By"] = detected_by.get("Name")
            detected_by_class = detected_by.get("Class")
            if detected_by_class:
                row["Detected By"] += f" ({detected_by_class})"
        if transitions:
            row["Transitions"] = json.dumps(transitions, indent=2)
        if resources:
            row["Resources"] = json.dumps(resources, indent=2)
        data.append(row)
    return tableToMarkdown("Incident Alerts", data)


def generate_human_readable_summarized_incidents_from_context(
    context_incidents: list[dict[str, Any]],
) -> str:
    """
    Generate a human-readable string for summarized GravityZone incidents from their context.
    Args:
        context_incidents (List[Dict[str, Any]]): The list of incident context data.
    Returns:
        str: The human-readable summarized incidents string.
    """
    processed_incidents = []
    for incident in context_incidents:
        processed_incident = {
            "ID": incident.get("ID"),
            "Number": incident.get("Number"),
            "Type": INCIDENT_TYPE_MAPPING.get(incident.get("Type", "unknown"), "Unknown"),
            "Company Name": incident.get("CompanyName"),
            "Severity Score": str(incident.get("SeverityScore")) + "%",
            "Status": str(incident.get("Status"))
            + " ("
            + INCIDENT_STATUS_STR_MAPPING.get(incident.get("RawJSON", {}).get("status", "unknown"), "unknown")
            + ")",
            "ActionTaken": incident.get("ActionTaken", "").capitalize(),
            "Created": incident.get("Created"),
            "Last Updated": incident.get("LastUpdated"),
            "Last Processed": incident.get("LastProcessed"),
            "Permalink": incident.get("Permalink"),
            "Assigned Priority": incident.get("AssignedPriority", "").capitalize(),
            "Assigned User ID": (incident.get("AssignedUserId") if incident.get("AssignedUserId") else "Unassigned"),
            "Attack Types": ", ".join(incident.get("AttackTypes", [])),
        }
        computer_id = incident.get("EndpointID")
        computer_name = incident.get("EndpointHostname")
        computer_ip = incident.get("EndpointIP")
        if computer_id:
            processed_incident["Endpoint ID"] = computer_id
        if computer_name:
            processed_incident["Endpoint Name"] = computer_name
        if computer_ip:
            processed_incident["Endpoint IP"] = computer_ip
        processed_incidents.append(processed_incident)
    return tableToMarkdown("GravityZone Incidents List", processed_incidents)


def get_node_by_id(node_id: str, incident: dict[str, Any]) -> dict[str, str] | None:
    """
    Retrieve a node by its ID from the incident details.
    Args:
        node_id (str): The ID of the node to retrieve.
        incident (Dict[str, Any]): The incident data containing nodes.
    Returns:
        Dict[str, str] | None: The node data if found, otherwise None.
    """
    for node in incident.get("details", {}).get("nodes", []):
        if node.get("id") == node_id:
            return node
    return None


def convert_from_gz_to_cortex(gz_incident: dict, include_json: bool) -> dict:
    """
    Convert a GravityZone incident to Cortex XSOAR incident format.
    Args:
        gz_incident (dict): The GravityZone incident data.
        include_json (bool): Whether to include the raw JSON in the output.
    Returns:
        dict: The converted Cortex XSOAR incident data.
    """
    gz_status = gz_incident.get("status", INCIDENT_STATUS_STR_IN_PROGRESS)
    status = INCIDENT_STATUS_INT_MAPPING.get(gz_status, IncidentStatus.PENDING)
    gz_incident["name"] = f"{gz_incident.get('incidentNumber', 'Unknown incident number')}"
    gz_incident_type = gz_incident.get("incidentType")
    incident = {
        "incidentId": gz_incident.get("incidentId"),
        "name": "GravityZone Incident #" + gz_incident["name"],
        "incident_type": gz_incident_type,
        "occurred": gz_incident.get("created"),
        "status": status,
    }
    params = demisto.params()
    mirroring_direction = params.get("mirror_direction")
    mirror_instance = demisto.integrationInstance()
    gz_incident["mirror_direction"] = mirroring_direction
    gz_incident["mirror_instance"] = mirror_instance
    gz_incident["mirror_tags"] = []

    if include_json:
        incident["rawJSON"] = json.dumps(gz_incident)
    return incident


def get_gz_status_matched_to_cortex_status(cortex_status: int, cortex_reason: str | None = None) -> int:
    """
    Map Cortex XSOAR incident status to GravityZone incident status.
    Args:
        cortex_status (int): The Cortex XSOAR incident status.
        cortex_reason (Optional[str]): The reason for the status, if applicable.
    Returns:
        int: The corresponding GravityZone incident status.
    """
    if cortex_reason and cortex_reason.lower() == "false positive":
        return GRAVITY_ZONE_INCIDENT_STATUS_FALSE_POSITIVE
    return INCIDENT_STATUS_MAPPING.get(cortex_status, GRAVITY_ZONE_INCIDENT_STATUS_OPEN)


def generate_human_readable_live_search_result(result: dict[str, Any], metadata: dict[str, Any] = {}) -> dict[str, Any]:
    """
    Generate a human-readable live search result from the raw result data.
    Args:
        result (Dict[str, Any]): The raw live search result data.
    Returns:
        Dict[str, Any]: The formatted live search result."""
    data = {
        "EndpointID": result.get("protectedEntityId"),
        "Results": result.get("results"),
    }
    # Enrich data with all fields in metadata
    if metadata:
        for k, v in metadata.items():
            k = "".join(word.capitalize() for word in k.split("_"))
            if k not in data and v is not None:
                data[k] = v
    return data


def generate_context_live_search_result(result: dict[str, Any], metadata: dict[str, Any] = {}) -> dict[str, Any]:
    """
    Generate a context live search result from the raw result data.
    Args:
        result (Dict[str, Any]): The raw live search result data.
    Returns:
        Dict[str, Any]: The formatted live search result."""
    results = result.get("results", {})
    if isinstance(results, str):
        try:
            json_data = json.loads(results)
        except Exception:
            json_data = {}
    else:
        json_data = results if isinstance(results, dict) else {}
    data = {
        "EndpointID": result.get("protectedEntityId"),
        "Cmdline": json_data.get("cmdline", None),
        "ParentPID": (int(json_data["parent"]) if json_data.get("parent") is not None else None),
        "Path": json_data.get("path", None),
        "PID": int(json_data["pid"]) if json_data.get("pid") is not None else None,
    }
    # Filter out None values from data
    data = {k: v for k, v in data.items() if v is not None}
    # Enrich data with all fields in metadata
    if metadata:
        for k, v in metadata.items():
            k = "".join(word.capitalize() for word in k.split("_"))
            if k not in data and v is not None:
                data[k] = v
    return data


def fill_task_output_with_metadata(data: dict[str, Any], task_type: str, metadata: dict[str, Any]) -> dict[str, Any]:
    """
    Fill the task output data with additional metadata based on the task type.
    Args:
        data (Dict[str, Any]): The task output data.
        task_type (str): The type of the task.
        metadata (Dict[str, Any]): The additional metadata.
    Returns:
        Dict[str, Any]: The updated task output data.
    """
    if task_type == COMMAND_KILL_PROCESS:
        process_pid = metadata.get("processId")
        process_path = metadata.get("path")
        data["ProcessID"] = process_pid if process_pid else -1
        data["ProcessPath"] = process_path if process_path else ""
    elif task_type == COMMAND_UPLOAD_FILE:
        local_file = metadata.get("localFile")
        destination_path = metadata.get("destinationPath")
        data["EntryID"] = local_file
        data["DestinationPath"] = destination_path
    return data


def fill_task_headers_with_metadata(task_type: str, metadata: dict[str, Any], headers: list) -> None:
    """
    Fill the task output headers with additional metadata based on the task type.
    Args:
        task_type (str): The type of the task.
        metadata (Dict[str, Any]): The additional metadata.
        headers (list): The list of headers to be updated.
    """
    if task_type == COMMAND_KILL_PROCESS:
        process_pid = metadata.get("processId")
        process_path = metadata.get("path")
        if process_pid:
            headers.append("ProcessID")
        if process_path:
            headers.append("ProcessPath")
    elif task_type == COMMAND_UPLOAD_FILE:
        headers.append("EntryID")
        headers.append("DestinationPath")


def generate_processed_task_command_result(
    task_id: str,
    task_output: dict,
    metadata: dict[str, Any],
    error_code: int | None = None,
) -> CommandResults:
    """
    Generate the CommandResults for a processed task.
    Args:
        task_id (str): The ID of the task.
        task_output (dict): The output data of the task.
        metadata (Dict[str, Any]): The additional metadata.
        error_code (Optional[int]): The error code if the task failed.
    Returns:
        CommandResults: The generated CommandResults object.
    """
    task_type = task_output.get("type", "")
    command_name_from_task_type = TASK_NUMERIC_TO_COMMAND_NAME.get(task_type, f"UnknownTask_{hex(task_type)}")
    task_output_prefix = f"GravityZone.Command.{command_name_from_task_type}"
    outputs = []
    hosts = []
    for subtask in task_output.get("subtasks") or []:
        data = generate_task_output(
            task_id=task_id,
            task_type=command_name_from_task_type,
            endpoint_id=subtask.get("endpointId"),
            status="Processed",
            end_date=subtask.get("endDate"),
            host_name=subtask.get("endpointName"),
            error_code=subtask.get("errorCode", "Success"),
            error=subtask.get("errorMessage", "Success"),
            start_date=subtask.get("startDate"),
        )
        outputs.append(fill_task_output_with_metadata(data, command_name_from_task_type, metadata))
        hosts.append(subtask.get("endpointId"))
    headers = TASK_OUTPUT_HEADERS.copy()
    fill_task_headers_with_metadata(command_name_from_task_type, metadata, headers)
    human_readable = tableToMarkdown(
        f"{task_output_prefix} command on hosts {', '.join(hosts)}:",
        outputs,
        headers=headers,
    )
    if error_code:
        response_entry_type = EntryType.ERROR
    else:
        response_entry_type = EntryType.NOTE
    return CommandResults(
        raw_response=task_output,
        readable_output=human_readable,
        outputs=outputs,
        outputs_prefix=task_output_prefix,
        outputs_key_field="TaskID",
        entry_type=response_entry_type,
    )


def generate_task_output(
    task_id: str,
    task_type: str,
    endpoint_id: str,
    status: str | None = None,
    end_date: str | None = None,
    host_name: str | None = None,
    error_code: str | None = None,
    error: str | None = None,
    start_date: str | None = None,
) -> dict[str, Any]:
    """
    Generate the output dictionary for a task.
    Args:
        task_id (str): The ID of the task.
        task_type (str): The type of the task.
        endpoint_id (str): The ID of the endpoint.
        status (Optional[str]): The status of the task.
        end_date (Optional[str]): The end date of the task.
        host_name (Optional[str]): The name of the host.
        error_code (Optional[str]): The error code if the task failed.
        error (Optional[str]): The error message if the task failed.
        start_date (Optional[str]): The start date of the task.
    Returns:
        Dict[str, Any]: The generated task output dictionary.
    """
    return {
        "TaskID": task_id,
        "TaskType": task_type,
        "Status": status if status else "Error",
        "EndDate": f"{end_date}Z" if end_date else datetime.now(UTC).strftime(DATE_FORMAT),
        "EndpointID": endpoint_id,
        "Hostname": host_name if host_name else "Unknown",
        "ErrorCode": error_code if error_code else "-1000",
        "Error": error if error else "Invalid Command Arguments",
        "StartDate": f"{start_date}Z" if start_date else datetime.now(UTC).strftime(DATE_FORMAT),
    }


def _extract_active_sessions_from_task(task_output: dict[str, Any]) -> list[dict[str, Any]]:
    sessions: list[dict[str, Any]] = []
    for subtask in task_output.get("subtasks") or []:
        if subtask.get("status") != TASK_STATUS_PROCESSED:
            continue

        for session in subtask.get("result", []) or []:
            user_data = session.get("user", {})
            username = user_data.get("displayName", "")

            row: dict[str, Any] = {
                "Username": username,
                "ConnectionType": session.get("connection", {}).get("type"),
                "StartTime": session.get("connection", {}).get("started"),
            }

            if user_data.get("sid"):
                row["UserSID"] = user_data.get("sid")

            domain_sid = user_data.get("domain", {}).get("sid")
            if domain_sid:
                row["DomainSID"] = domain_sid

            ou_dn = user_data.get("organizationalUnit", {}).get("distinguishedName")
            if ou_dn:
                row["OrganizationalUnitDN"] = ou_dn

            member_of_sids = [
                group.get("sid") for group in user_data.get("memberOf", []) if isinstance(group, dict) and group.get("sid")
            ]
            if member_of_sids:
                row["MemberOfSIDs"] = member_of_sids

            sessions.append(row)

    return sessions


def _extract_endpoint_summary_from_task(task_output: dict[str, Any], endpoint_id: str) -> tuple[str, str]:
    resolved_endpoint_id = endpoint_id
    resolved_hostname = ""

    for subtask in task_output.get("subtasks") or []:
        subtask_endpoint_id = subtask.get("endpointId") or ""
        subtask_hostname = subtask.get("endpointName") or ""

        if not resolved_endpoint_id and subtask_endpoint_id:
            resolved_endpoint_id = subtask_endpoint_id

        if resolved_endpoint_id and subtask_endpoint_id == resolved_endpoint_id:
            resolved_hostname = subtask_hostname
            break

        if not resolved_hostname and subtask_hostname:
            resolved_hostname = subtask_hostname

    return resolved_endpoint_id, resolved_hostname


def _validate_task_type_for_command(
    task_output: dict[str, Any],
    task_id: str,
    expected_task_type: int,
    command_name: str,
) -> CommandResults | None:
    task_type = task_output.get("type")
    if task_type is None:
        return CommandResults(
            raw_response=task_output,
            readable_output=(
                f"Task '{task_id}' does not have a type. "
                f"Cannot determine if it matches the expected type for command '{command_name}'."
            ),
            entry_type=EntryType.ERROR,
        )

    if task_type == expected_task_type:
        return None

    expected_task_name = TASK_NUMERIC_TO_COMMAND_NAME.get(expected_task_type, str(expected_task_type))
    received_task_name = (
        TASK_NUMERIC_TO_COMMAND_NAME.get(task_type, str(task_type)) if isinstance(task_type, int) else str(task_type)
    )
    return CommandResults(
        raw_response=task_output,
        readable_output=(
            f"Task '{task_id}' has unexpected type '{received_task_name}' ({task_type}). "
            f"Command '{command_name}' supports only '{expected_task_name}' ({expected_task_type}) tasks."
        ),
        entry_type=EntryType.ERROR,
    )


def _build_users_loggedin_results(task_output: dict[str, Any], endpoint_id: str) -> CommandResults:
    """Build endpoint-scoped command results for active logged-in sessions."""
    endpoint_id, endpoint_hostname = _extract_endpoint_summary_from_task(task_output, endpoint_id)

    sessions = _extract_active_sessions_from_task(task_output)
    endpoint_output: dict[str, Any] = {
        "ID": endpoint_id,
        "Hostname": endpoint_hostname,
        "ActiveSessions": sessions or [],
    }

    if not sessions:
        return CommandResults(
            raw_response=task_output,
            readable_output=f"No active sessions found on endpoint {endpoint_id}.",
            outputs=endpoint_output,
            outputs_prefix="GravityZone.Endpoint",
            outputs_key_field="ID",
            entry_type=EntryType.NOTE,
        )

    return CommandResults(
        raw_response=task_output,
        readable_output=tableToMarkdown(
            f"Active sessions on endpoint {endpoint_id}",
            sessions,
            headers=[
                "Username",
                "ConnectionType",
                "StartTime",
                "UserSID",
                "DomainSID",
                "OrganizationalUnitDN",
                "MemberOfSIDs",
            ],
        ),
        outputs=endpoint_output,
        outputs_prefix="GravityZone.Endpoint",
        outputs_key_field="ID",
        entry_type=EntryType.NOTE,
    )


def _extract_memory_dump_summary(
    task_output: dict[str, Any],
    endpoint_id: str,
) -> tuple[str, str, dict[str, Any] | None, str]:
    resolved_endpoint_id = endpoint_id
    resolved_hostname = ""
    fallback_subtask: dict[str, Any] = {}
    processed_subtask: dict[str, Any] = {}
    download_url = ""

    for subtask in task_output.get("subtasks") or []:
        subtask_endpoint_id = subtask.get("endpointId") or ""
        subtask_hostname = subtask.get("endpointName") or ""

        if endpoint_id and subtask_endpoint_id != endpoint_id:
            continue

        if not resolved_endpoint_id and subtask_endpoint_id:
            resolved_endpoint_id = subtask_endpoint_id

        if not resolved_hostname and subtask_hostname:
            resolved_hostname = subtask_hostname

        if not fallback_subtask:
            fallback_subtask = subtask

        if subtask.get("status") == TASK_STATUS_PROCESSED:
            if not processed_subtask:
                processed_subtask = subtask
            if not download_url:
                download_url = subtask.get("downloadURL") or ""

        if resolved_endpoint_id and subtask_endpoint_id == resolved_endpoint_id and subtask_hostname:
            resolved_hostname = subtask_hostname

    return resolved_endpoint_id, resolved_hostname, (processed_subtask or fallback_subtask or None), download_url


def _build_memory_dump_results(
    task_output: dict[str, Any],
    task_id: str,
    endpoint_id: str,
    readable_headers: list[str] | None = None,
) -> CommandResults:
    """Build endpoint-scoped command results for memory dump status polling."""
    endpoint_id, endpoint_hostname, memory_dump_subtask, download_url = _extract_memory_dump_summary(task_output, endpoint_id)

    if memory_dump_subtask is None:
        available_endpoint_ids = [
            subtask.get("endpointId") for subtask in (task_output.get("subtasks") or []) if subtask.get("endpointId")
        ]
        if endpoint_id:
            return CommandResults(
                raw_response=task_output,
                readable_output=f"Invalid Endpoint ID. Available endpoint IDs for task '{task_id}': {available_endpoint_ids}",
                entry_type=EntryType.ERROR,
            )

        return CommandResults(
            raw_response=task_output,
            readable_output=f"Task '{task_id}' has no memory dump results.",
            entry_type=EntryType.ERROR,
        )

    status = MEMORY_DUMP_STATUS_PROCESSED if task_output.get("status") == TASK_STATUS_PROCESSED else MEMORY_DUMP_STATUS_PENDING
    subtask_end_date = memory_dump_subtask.get("endDate")
    subtask_error_code = memory_dump_subtask.get("errorCode")
    subtask_error = memory_dump_subtask.get("errorMessage")

    endpoint_output = generate_task_output(
        task_id=task_id,
        task_type=COMMAND_CREATE_MEMORY_DUMP,
        endpoint_id=endpoint_id,
        status=status,
        end_date=subtask_end_date,
        host_name=endpoint_hostname,
        error_code=subtask_error_code,
        error=subtask_error,
        start_date=memory_dump_subtask.get("startDate"),
    )
    if status == MEMORY_DUMP_STATUS_PENDING:
        endpoint_output["EndDate"] = f"{subtask_end_date}Z" if subtask_end_date else ""
        endpoint_output["ErrorCode"] = subtask_error_code or ""
        endpoint_output["Error"] = subtask_error or ""
    endpoint_output["DownloadURL"] = download_url

    return CommandResults(
        raw_response=task_output,
        readable_output=tableToMarkdown(
            f"Memory dump for endpoint {endpoint_id}",
            [endpoint_output],
            headers=readable_headers if readable_headers is not None else TASK_OUTPUT_HEADERS + ["DownloadURL"],
        ),
        outputs=endpoint_output,
        outputs_prefix="GravityZone.MemoryDump",
        outputs_key_field="EndpointID",
        entry_type=EntryType.NOTE,
    )


def _extract_memory_dump_task_id(task_data: Any) -> str:
    if isinstance(task_data, dict):
        task_id = task_data.get("taskId")
        if isinstance(task_id, str) and task_id:
            return task_id

    if not isinstance(task_data, str) or not task_data:
        raise DemistoException("createMemoryDumpTask response is missing task ID.")

    return task_data


def fetch_incidents(client: Client, start_fetch_time, end_fetch_time, fetch_limit=FETCH_LIMIT) -> list[dict]:
    """
    Fetches incidents from GravityZone within the specified time range.
    Args:
        client (Client): The GravityZone client instance.
        start_fetch_time (str): The start time for fetching incidents.
        end_fetch_time (str): The end time for fetching incidents.
        fetch_limit (int): The maximum number of incidents to fetch.
    Returns:
        list[dict]: A list of fetched incidents.
    """
    incidents: list[dict[str, Any]] = []

    gz_incidents = client.get_incidents(
        start_time=start_fetch_time,
        end_time=end_fetch_time,
        max_fetch=fetch_limit,
    )

    for incident in gz_incidents:
        incident["occurred"] = incident["created"]

    params = demisto.params()
    mirroring_direction = params.get("mirror_direction")
    mirror_instance = demisto.integrationInstance()

    for gz_incident in gz_incidents:
        gz_incident["mirror_direction"] = mirroring_direction
        gz_incident["mirror_instance"] = mirror_instance
        gz_incident["mirror_tags"] = []
        incident = convert_from_gz_to_cortex(gz_incident, include_json=True)
        incidents.append(incident)

    return incidents


def get_entries(new_incident: dict, old_incident: dict) -> list[dict]:
    """
    Generate entries for incident status changes.
    Args:
        new_incident (dict): The updated incident data.
        old_incident (dict): The previous incident data.
    Returns:
        list[dict]: A list of entries reflecting the status change.
    """

    if not new_incident or not old_incident:
        return []

    new_status = new_incident.get("status")
    old_status = old_incident.get("status")

    if new_status == old_status:
        return []

    if new_status == IncidentStatus.DONE:
        return [
            {
                "Type": EntryType.NOTE,
                "Contents": {
                    "closeReason": "Incident was resolved in GravityZone platform",
                    "dbotIncidentClose": True,
                },
                "ContentsFormat": EntryFormat.JSON,
            }
        ]

    return [
        {
            "Type": EntryType.NOTE,
            "Contents": {"dbotIncidentReopen": True},
            "ContentsFormat": EntryFormat.JSON,
        }
    ]


""" COMMAND FUNCTIONS """


@logger
def test_module(client: Client, args: dict[str, Any]) -> str:
    """
    Test the GravityZone client connectivity and fetch incidents.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
    Returns:
        str: "ok" if the test is successful.
    """
    try:
        params = demisto.params()
        now_dt = datetime.now(UTC)
        is_fetch = argToBoolean(params.get("isFetch", False))
        if is_fetch:
            first_fetch_time = (now_dt - timedelta(minutes=1)).strftime(GZ_DATE_FORMAT)
            fetch_incidents(
                client=client,
                start_fetch_time=first_fetch_time,
                end_fetch_time=now_dt.strftime(GZ_DATE_FORMAT),
                fetch_limit=1,
            )
        else:
            start_time = (now_dt - timedelta(minutes=5)).strftime(GZ_DATE_FORMAT)
            end_time = (now_dt + timedelta(minutes=5)).strftime(GZ_DATE_FORMAT)
            client.get_incidents(start_time=start_time, end_time=end_time)

        return "ok"
    except Exception as e:
        return f"Test failed: {str(e)}"


@logger
def gz_poll_task_status_command(args: dict[str, Any], client: Client) -> PollResult:
    """
    Poll the status of a task in GravityZone.
    Args:
        args (Dict[str, Any]): The command arguments.
        client (Client): The GravityZone client instance.
    Returns:
        PollResult: The result of the task status polling.
    """
    return check_task_status(args, client)


@logger
def gz_poll_investigation_activity_status_command(args: dict[str, Any], client: Client) -> PollResult:
    """
    Poll the status of an investigation activity in GravityZone.
    Args:
        args (Dict[str, Any]): The command arguments.
        client (Client): The GravityZone client instance.
    Returns:
        PollResult: The result of the investigation activity status polling.
    """
    return check_investigation_status(args, client)


@logger
def gz_poll_live_search_status_command(args: dict[str, Any], client: Client) -> PollResult:
    """
    Poll the status of a live search in GravityZone.
    Args:
        args (Dict[str, Any]): The command arguments.
        client (Client): The GravityZone client instance.
    Returns:
        PollResult: The result of the live search status polling.
    """
    return query_live_search_results(args, client)


@logger
def get_mapping_fields_command(client: Client, args: dict[str, Any]) -> GetMappingFieldsResponse:
    """
    Get the mapping fields for GravityZone incidents.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
    Returns:
        GetMappingFieldsResponse: The mapping fields response.
    """
    xdr_incident_type_scheme = SchemeTypeMapping(type_name="GravityZone XDR")
    xdr_incident_type_scheme.add_field(name="status", description="Incident status")

    edr_incident_type_scheme = SchemeTypeMapping(type_name="GravityZone EDR")
    edr_incident_type_scheme.add_field(name="status", description="Incident status")

    return GetMappingFieldsResponse([xdr_incident_type_scheme, edr_incident_type_scheme])


@logger
def fetch_incidents_command(client: Client, args: dict[str, Any]) -> None:
    """
    Fetch incidents from GravityZone and set them in Demisto.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
    Returns:
        None
    """
    params = demisto.params()

    incidents = []

    fetch_limit_param = params.get("max_fetch", FETCH_LIMIT)
    look_back = arg_to_number(params.get("look_back")) or 300
    first_fetch = params.get("first_fetch", "3 days")
    time_zone = arg_to_number(params.get("time_zone")) or 0

    last_run = demisto.getLastRun()

    fetch_limit = arg_to_number(last_run.get("limit", None) or fetch_limit_param) or FETCH_LIMIT
    start_fetch_time, end_fetch_time = get_fetch_run_time_range(
        last_run=last_run,
        first_fetch=first_fetch,
        look_back=look_back,
        timezone=time_zone,
        date_format=GZ_DATE_FORMAT,
    )

    incidents_res = fetch_incidents(
        client=client,
        start_fetch_time=start_fetch_time,
        end_fetch_time=end_fetch_time,
        fetch_limit=fetch_limit,
    )

    incidents = filter_incidents_by_duplicates_and_limit(
        incidents_res=incidents_res,
        last_run=last_run,
        fetch_limit=fetch_limit,
        id_field="incidentId",
    )

    last_run = update_last_run_object(
        last_run=last_run,
        incidents=incidents,
        fetch_limit=fetch_limit,
        start_fetch_time=start_fetch_time,
        end_fetch_time=end_fetch_time,
        look_back=look_back,
        created_time_field="occurred",
        id_field="incidentId",
        date_format=GZ_DATE_FORMAT,
    )

    demisto.incidents(incidents)
    demisto.setLastRun(last_run)


@logger
def get_modified_remote_data_command(client: Client, args: dict[str, Any]) -> GetModifiedRemoteDataResponse:
    """
    Get the IDs of modified remote incidents from GravityZone.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
    Returns:
        GetModifiedRemoteDataResponse: The response containing modified incident IDs.
    """
    remote_args = GetModifiedRemoteDataArgs(args)

    last_update_utc = dateparser.parse(remote_args.last_update, settings={"TIMEZONE": "UTC"}) or datetime.now(UTC)
    start_time = (last_update_utc - timedelta(days=LOOPBACK_DAYS)).strftime(GZ_DATE_FORMAT)
    end_time = (datetime.now(UTC) + timedelta(days=LOOPBACK_DAYS)).strftime(GZ_DATE_FORMAT)

    raw_incidents = client.get_incidents(
        start_time=start_time,
        end_time=end_time,
        max_fetch=1000,
    )
    modified_incident_ids = [incident.get("incidentId") for incident in raw_incidents]
    modified_incident_ids = sorted(modified_incident_ids, key=lambda x: str(x))
    return GetModifiedRemoteDataResponse(modified_incident_ids)


@logger
def get_remote_data_command(client: Client, args: dict[str, Any]) -> GetRemoteDataResponse | None:
    """
    Get the remote incident data from GravityZone and generate entries for status changes.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
    Returns:
        GetRemoteDataResponse: The response containing the incident data and entries.
    """
    parsed_args = GetRemoteDataArgs(args)
    old_incident = demisto.investigation()
    new_incident_raw_data = client.get_incident(parsed_args.remote_incident_id)
    new_incident_data = convert_from_gz_to_cortex(new_incident_raw_data, include_json=True)
    parsed_entries = get_entries(new_incident=new_incident_data, old_incident=old_incident)
    new_incident_data["id"] = new_incident_data["incidentId"]
    return GetRemoteDataResponse(new_incident_data, parsed_entries)


@logger
def update_remote_system_command(client: Client, args: dict[str, Any]) -> str:
    """
    Update the remote incident in GravityZone based on changes in Demisto.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
    Returns:
        str: The remote incident ID.
    """
    parsed_args = UpdateRemoteSystemArgs(args)

    incident_id: str = parsed_args.remote_incident_id

    try:
        if not parsed_args.incident_changed:
            return incident_id

        incident = client.get_incident(incident_id)
        if not incident:
            raise DemistoException(f"Incident {incident_id} was not found")

        if parsed_args.delta.get("closeNotes"):
            incident_type = incident.get("incidentType", "incident")
            client.add_incident_note(incident_type, incident_id, parsed_args.delta.get("closeNotes"))

        gz_status = get_gz_status_matched_to_cortex_status(parsed_args.inc_status, parsed_args.delta.get("closeReason"))
        existing_cortex_status = INCIDENT_STATUS_INT_MAPPING.get(
            incident.get("status", INCIDENT_STATUS_STR_OPEN), IncidentStatus.PENDING
        )
        existing_gz_status = INCIDENT_STATUS_MAPPING.get(existing_cortex_status, GRAVITY_ZONE_INCIDENT_STATUS_OPEN)
        if gz_status != existing_gz_status:
            client.change_incident_status(incident_id, gz_status)

    except Exception:
        pass

    return incident_id


@logger
def gz_incident_get_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Get an incident from GravityZone by its ID.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID of the incident to retrieve.
    Returns:
        CommandResults: The command results containing the incident data.
    """
    incident_id = args.get("id", "UNKNOWN_INCIDENT_ID")

    raw_response = client.get_incident(incident_id)
    context_data = generate_context_for_incident(raw_response)
    human_readable = generate_human_readable_incident_from_context(context_data)
    return CommandResults(
        readable_output=human_readable,
        outputs_prefix="GravityZone.Incident",
        outputs_key_field="ID",
        outputs=context_data,
        raw_response=raw_response,
        replace_existing=True,
    )


@logger
def gz_incident_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    List incidents from GravityZone within a specified time range and optional endpoint ID.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            from_date (str, optional): The start time for filtering incidents. Default is 3 days ago.
            to_date (str, optional): The end time for filtering incidents. Default is now.
            limit (int, optional): The maximum number of incidents to retrieve. Default is 50.
            endpoint_id (str, optional): The ID of the endpoint to filter incidents.
    Returns:
        CommandResults: The command results containing the list of incidents.
    """
    endpoint_id = args.get("endpoint_id", "")
    from_date = arg_to_datetime(args.get("from_date")) or (datetime.now(UTC) - timedelta(days=GET_INCIDENTS_DELTA_DAYS))
    to_date = arg_to_datetime(args.get("to_date")) or (datetime.now(UTC) + timedelta(minutes=5))
    limit = arg_to_number(args.get("limit")) or FETCH_LIMIT
    incidents = []

    start_time = from_date.strftime(GZ_DATE_FORMAT)
    end_time = to_date.strftime(GZ_DATE_FORMAT)

    incidents = client.get_incidents(
        start_time=start_time,
        end_time=end_time,
        max_fetch=limit,
        target_id=endpoint_id if endpoint_id else None,
    )
    context_data = generate_context_for_summarized_incidents(incidents)
    readable_output = generate_human_readable_summarized_incidents_from_context(context_data)
    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="GravityZone.IncidentsList",
        outputs_key_field="ID",
        outputs=context_data,
        raw_response=incidents,
    )


@logger
def gz_incident_add_note_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Add a note to an incident in GravityZone.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID(s) of the incident(s) to add the note to.
            note (str): The note text to add.
    Returns:
        CommandResults: The command results containing the status of the note addition.
    """
    incident_ids = argToList(args.get("id", []))
    note = args.get("note", "")

    command = "AddIncidentNote"
    output_prefix = f"GravityZone.Command.{command}"
    outputs = []
    raw_responses = []

    for incident_id in incident_ids:
        output = {
            "IncidentID": incident_id,
            "Note": note,
        }
        raw_response = {
            "incident_id": incident_id,
            "note": note,
        }
        try:
            incident = client.get_incident(incident_id)
            if not incident:
                raise DemistoException(f"Incident {incident_id} was not found")
            incident_type = incident.get("incidentType", "incident")
            result = client.add_incident_note(incident_type, incident_id, note)
            if not result:
                raise DemistoException(f"Incident {incident_id} note was not added successfully")
            status = "Success"
        except Exception:
            status = "Cannot add incident note"
        output["CommandStatus"] = status
        raw_response["command_status"] = status
        outputs.append(output)
        raw_responses.append(raw_response)

    return CommandResults(
        raw_response=raw_responses,
        readable_output=tableToMarkdown(
            f"{output_prefix} command on incidents {', '.join(incident_ids)}:",
            outputs,
            headers=["IncidentID", "Note", "CommandStatus"],
        ),
        outputs=outputs,
        outputs_prefix=output_prefix,
        outputs_key_field="IncidentID",
        entry_type=EntryType.NOTE,
    )


@logger
def gz_incident_change_status_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Change the status of an incident in GravityZone.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID(s) of the incident(s) to change the status of.
            status (str): The new status to set for the incident(s).
    Returns:
        CommandResults: The command results containing the status of the status change.
    """
    incident_ids = argToList(args.get("id", []))
    status = args.get("status", "PENDING")

    valid_statuses = {
        "PENDING": IncidentStatus.PENDING,
        "ACTIVE": IncidentStatus.ACTIVE,
        "DONE": IncidentStatus.DONE,
        "ARCHIVE": IncidentStatus.ARCHIVE,
    }

    cortex_status = valid_statuses[status]
    gz_status = INCIDENT_STATUS_MAPPING.get(cortex_status, GRAVITY_ZONE_INCIDENT_STATUS_OPEN)

    command = "ChangeIncidentStatus"
    output_prefix = f"GravityZone.Command.{command}"
    outputs, raw_responses = [], []

    for incident_id in incident_ids:
        output = {"IncidentID": incident_id, "IncidentStatus": status}
        raw_response = {"incident_id": incident_id, "incident_status": status}
        try:
            result = client.change_incident_status(incident_id, gz_status)
            if not result:
                raise DemistoException(f"Incident {incident_id} status was not updated")
            result_status = "Success"
        except Exception:
            result_status = "Cannot update incident status"
        output["CommandStatus"] = result_status
        raw_response["command_status"] = result_status
        outputs.append(output)
        raw_responses.append(raw_response)

    return CommandResults(
        raw_response=raw_responses,
        readable_output=tableToMarkdown(
            f"{output_prefix} command on incidents {', '.join(incident_ids)}:",
            outputs,
            headers=["IncidentID", "IncidentStatus", "CommandStatus"],
        ),
        outputs=outputs,
        outputs_prefix=output_prefix,
        outputs_key_field="IncidentID",
        entry_type=EntryType.NOTE,
    )


@logger
def gz_endpoint_get_process_tree_by_hash_command(client: Client, args: dict[str, Any]) -> PollResult:
    """
    Get the process tree for a given process hash on a specific endpoint.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID of the endpoint.
            process_hash (str): The hash of the process to search for.
    Returns:
        PollResult: The result of the live search for the process tree.
    """
    process_hash = args.get("process_hash", "UNKNOWN_PROCESS_HASH").lower()
    endpoint_id = args.get("id", "")

    task_id = client.start_live_search_query_find_running_process_tree_by_hash([endpoint_id], process_hash)

    return get_live_search_results(client, task_id, LIVE_SEARCH_QUERY_PROCESS_PER_HASH, metadata="{}")


@logger
def gz_endpoint_list_by_running_process_hash_command(client: Client, args: dict[str, Any]) -> PollResult:
    """
    Get the endpoints running a specific process hash.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            process_hash (str): The hash of the process to search for.
    Returns:
        PollResult: The result of the live search for the endpoints running the process hash.
    """
    process_hash = args.get("process_hash", "UNKNOWN_PROCESS_HASH").lower()

    task_id = client.start_live_search_query_find_endpoints_running_process_by_hash([], process_hash)

    return get_live_search_results(
        client,
        task_id,
        LIVE_SEARCH_QUERY_RUNNING_HASH,
        metadata=json.dumps({"process_hash": process_hash}),
    )


@logger
def gz_endpoint_list_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    List all endpoints in GravityZone.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            limit (int): The maximum number of endpoints to retrieve. Default is 100.
    Returns:
        CommandResults: The command results containing the list of endpoints.
    """
    limit = arg_to_number(args.get("limit")) or ENDPOINTS_PER_PAGE
    name = args.get("name", None)
    if name:
        if len(name.strip("*")) < 3:
            raise DemistoException("The 'name' argument must be at least 3 characters long.")
        if "*" in name and not name.startswith("*"):
            raise DemistoException("If using a wildcard, the 'name' argument must start with '*'.")
        if name.count("*") > 1:
            raise DemistoException("The 'name' argument can only contain one wildcard '*' character.")
    node_id = args.get("node_id", None)
    endpoints_details = client.get_endpoints(limit=limit, name=name, node_id=node_id)
    raw_endpoints = []
    outputs = []
    for endpoint_details in endpoints_details:
        raw_endpoints.append(endpoint_details)
        outputs.append(generate_endpoint_entry_from_list(endpoint_details))

    return CommandResults(
        readable_output=tableToMarkdown(
            "GravityZone Endpoints List",
            outputs,
            headers=["ID", "Hostname", "IP", "OS", "MAC", "Vendor", "FQDN", "CompanyID", "GroupID"],
        ),
        raw_response=raw_endpoints,
        outputs=outputs,
        outputs_prefix="GravityZone.EndpointsList",
        outputs_key_field="ID",
        entry_type=EntryType.NOTE,
    )


@logger
def gz_endpoint_get_command(client: Client, args: dict[str, Any]) -> CommandResults:
    """
    Get an endpoint from GravityZone by its ID.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID of the endpoint to retrieve.
    Returns:
        CommandResults: The command results containing the endpoint data.
    """
    endpoint_id = args.get("id", "")

    endpoint_details = client.get_endpoint(endpoint_id)
    entry = generate_endpoint_entry(endpoint_details)
    endpoint = generate_endpoint_by_contex_standard(endpoint_details)
    output_prefix = "GravityZone.Endpoint"

    return CommandResults(
        readable_output=tableToMarkdown(
            "GravityZone Endpoint",
            entry,
            headers=[
                "ID",
                "Hostname",
                "IP",
                "OS",
                "Status",
                "Vendor",
                "LastLoggedUsers",
            ],
        ),
        raw_response=endpoint_details,
        indicator=endpoint,
        outputs=entry,
        outputs_prefix=output_prefix,
        outputs_key_field="ID",
        entry_type=EntryType.NOTE,
    )


@logger
def gz_endpoint_isolate_command(client: Client, args: dict[str, Any]) -> PollResult:
    """
    Isolate an endpoint in GravityZone by its ID.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID of the endpoint to isolate.
    Returns:
        PollResult: The command results of the isolation task.
    """
    endpoint_id = args.get("id", "")

    result = client.start_isolate_endpoint(endpoint_id)

    return get_task_results(client, result[0], {"endpointId": endpoint_id})


@logger
def gz_endpoint_deisolate_command(client: Client, args: dict[str, Any]) -> PollResult:
    """
    Deisolate an endpoint in GravityZone by its ID.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID of the endpoint to deisolate.
    Returns:
        PollResult: The command results of the deisolation task.
    """
    endpoint_id = args.get("id", "")

    result = client.start_deisolate_endpoint(endpoint_id)

    return get_task_results(client, result[0], {"endpointId": endpoint_id})


@logger
def gz_endpoint_run_command_command(client: Client, args: dict[str, Any]) -> PollResult:
    """
    Run a command on an endpoint in GravityZone by its ID.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID of the endpoint.
            command (str): The command to run on the endpoint.
    Returns:
        CommandResults: The command results of the command execution.
    """
    endpoint_id = args.get("id", "")
    command = args.get("command", "")

    command_execution_activity_id = client.start_command_execution_on_endpoint(endpoint_id, command)

    return get_investigation_results(
        client,
        endpoint_id,
        command_execution_activity_id,
        "",
        {
            "activityType": ACTIVITY_TYPE_RUN_COMMAND,
            "targetId": endpoint_id,
            "command": command,
        },
    )


@logger
def gz_endpoint_download_file_command(client: Client, args: dict[str, Any]) -> PollResult:
    """
    Download a file from an endpoint in GravityZone by its ID.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID of the endpoint.
            remote_file (str): The full path of the remote file to download.
            output_file (str): The name of the output file.
    Returns:
        PollResult: The command results of the file download.
    """
    endpoint_id = args.get("id", "")
    remote_file = args.get("remote_file", "")
    output_file = args.get("output_file", "")

    retrieve_activity_id = client.start_retrieve_investigation_file_from_endpoint(endpoint_id, remote_file)

    return get_investigation_results(
        client,
        endpoint_id,
        retrieve_activity_id,
        output_file,
        {
            "activityType": ACTIVITY_TYPE_DOWNLOAD_FILE,
            "targetId": endpoint_id,
            "remoteFile": remote_file,
            "outputFile": output_file,
        },
    )


@logger
def gz_endpoint_upload_file_command(client: Client, args: dict[str, Any]) -> PollResult:
    """
    Upload a file to an endpoint in GravityZone by its ID.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID of the endpoint.
            entry_id (str): The ID of the file entry to upload.
            remote_location (str): The destination path on the endpoint.
    Returns:
        PollResult: The command results of the file upload.
    """
    endpoint_id = args.get("id", "")
    entry_id = args.get("entry_id", "")
    remote_location = args.get("remote_location", "")

    response = FileManagement(client).upload_file(
        "investigationFiles",
        entry_id,
        {"protectedEntityId": endpoint_id, "path": remote_location},
    )
    upload_task_id = response["additionalData"]["taskId"]

    return get_task_results(
        client,
        upload_task_id,
        {
            "localFile": entry_id,
            "endpointId": endpoint_id,
            "destinationPath": remote_location,
        },
    )


@logger
def gz_endpoint_download_investigation_package_command(client: Client, args: dict[str, Any]) -> PollResult:
    """
    Download an investigation package from an endpoint in GravityZone by its ID.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID of the endpoint.
            output_file (str): The name of the output file.
    Returns:
        PollResult: The command results of the investigation package download.
    """
    endpoint_id = args.get("id", "")
    output_file = args.get("output_file", "")

    result = client.start_collect_investigation_package_on_endpoint(endpoint_id)

    if "activityId" in result:
        activity_id = result["activityId"]
    else:
        return PollResult(
            CommandResults(
                raw_response=result,
                readable_output=f"Collect investigation package for endpoint '{endpoint_id}' failed.",
                entry_type=EntryType.ERROR,
            )
        )

    return get_investigation_results(
        client,
        endpoint_id,
        activity_id,
        output_file,
        {
            "activityType": ACTIVITY_TYPE_DOWNLOAD_FILE,
            "targetId": endpoint_id,
            "outputFile": output_file,
        },
    )


@logger
def gz_endpoint_kill_process_command(client: Client, args: dict[str, Any]) -> PollResult:
    """
    Kill a process on an endpoint in GravityZone by its Process ID.
    Args:
        client (Client): The GravityZone client instance.
        args (Dict[str, Any]): The command arguments.
            id (str): The ID of the endpoint.
            pid (str): The Process ID of the process to be killed.
    Returns:
        PollResult: The command results of the process kill.
    """
    endpoint_id = args.get("id", "")
    process_id = arg_to_number(args.get("pid"))
    if process_id is None:
        return PollResult(
            CommandResults(
                readable_output="Invalid process ID.",
                entry_type=EntryType.ERROR,
            )
        )

    kill_task_id = client.start_kill_process_on_endpoint(endpoint_id, process_id)

    return get_task_results(client, kill_task_id, {"targetId": endpoint_id, "processId": process_id})


@polling_function(
    name="gz-poll-endpoint-users-loggedin-status",
    timeout=POLL_TIMEOUT,
    interval=POLL_INTERVAL,
    requires_polling_arg=False,
)
def check_endpoint_users_loggedin_status(args: dict[str, Any], client: Client) -> PollResult:
    task_id = args.get("task_id", "")
    endpoint_id = args.get("endpoint_id", "")
    if not task_id:
        return PollResult(
            CommandResults(
                readable_output="`task_id` must be provided.",
                entry_type=EntryType.ERROR,
            )
        )

    task_output = client.get_task_status(task_id)
    invalid_task_type_result = _validate_task_type_for_command(
        task_output,
        task_id,
        TASK_TYPE_GET_ACTIVE_SESSIONS_PARENT,
        "gz-poll-endpoint-users-loggedin-status",
    )
    if invalid_task_type_result:
        return PollResult(invalid_task_type_result)

    current_status = task_output.get("status")

    if current_status == TASK_STATUS_PROCESSED:
        return PollResult(_build_users_loggedin_results(task_output, endpoint_id))

    return PollResult(
        continue_to_poll=True,
        response=CommandResults(
            raw_response=task_output,
            readable_output=f"Task '{task_id}' still pending.",
            entry_type=EntryType.NOTE,
        ),
    )


def gz_poll_endpoint_users_loggedin_status_command(args: dict[str, Any], client: Client) -> PollResult:
    return check_endpoint_users_loggedin_status(args, client)


@logger
def gz_endpoint_users_loggedin_command(client: Client, args: dict[str, Any]) -> PollResult:
    endpoint_id = args.get("id", "")
    task_id = client.start_get_active_sessions_on_endpoint(endpoint_id)

    return check_endpoint_users_loggedin_status({"task_id": task_id, "endpoint_id": endpoint_id}, client)


@polling_function(
    name="gz-endpoint-memory-dump-status",
    timeout=MEMORY_DUMP_POLL_TIMEOUT,
    interval=POLL_INTERVAL,
    requires_polling_arg=True,
)
def check_endpoint_memory_dump_status(args: dict[str, Any], client: Client) -> PollResult:
    task_id = args.get("task_id", "")
    endpoint_id = args.get("endpoint_id", "")
    if not task_id:
        return PollResult(
            CommandResults(
                readable_output="`task_id` must be provided.",
                entry_type=EntryType.ERROR,
            )
        )

    task_output = client.get_task_status(task_id)
    invalid_task_type_result = _validate_task_type_for_command(
        task_output,
        task_id,
        TASK_TYPE_CREATE_MEMORY_DUMP_PARENT,
        "gz-endpoint-memory-dump-status",
    )
    if invalid_task_type_result:
        return PollResult(invalid_task_type_result)

    current_status = task_output.get("status")

    if current_status == TASK_STATUS_PROCESSED:
        return PollResult(_build_memory_dump_results(task_output, task_id, endpoint_id))

    return PollResult(
        continue_to_poll=True,
        response=CommandResults(
            raw_response=task_output,
            readable_output=f"Task '{task_id}' still pending.",
            entry_type=EntryType.NOTE,
        ),
    )


@logger
def gz_endpoint_memory_dump_status_command(client: Client, args: dict[str, Any]) -> PollResult:
    if "polling" not in args:
        args = {**args, "polling": True}
    return check_endpoint_memory_dump_status(args, client)


@logger
def gz_endpoint_create_memory_dump_command(client: Client, args: dict[str, Any]) -> PollResult | CommandResults:
    endpoint_id = args.get("id", "")
    path = args.get("path", "")
    password = args.get("password", "")
    polling = argToBoolean(args.get("polling", True))

    task_response = client.start_create_memory_dump_on_endpoint(endpoint_id, path, password)
    task_id = _extract_memory_dump_task_id(task_response)

    if polling:
        return check_endpoint_memory_dump_status(
            {
                "task_id": task_id,
                "endpoint_id": endpoint_id,
                "polling": True,
            },
            client,
        )

    task_output = (
        task_response
        if isinstance(task_response, dict)
        else {"taskId": task_id, "status": TASK_STATUS_PENDING, "subtasks": [{"endpointId": endpoint_id}]}
    )
    return _build_memory_dump_results(task_output, task_id, endpoint_id, readable_headers=["TaskID", "EndpointID"])


def main():
    command: str = demisto.command()

    try:
        params = demisto.params()
        args = demisto.args()

        base_url = params.get("url", "")
        api_key = params.get("credentials", {}).get("password")
        verify_certificate = not argToBoolean(params.get("insecure", False))
        proxy = argToBoolean(params.get("proxy", False))

        client = Client(
            url=base_url,
            api_key=api_key,
            verify=verify_certificate,
            proxy=proxy,
        )

        # Map command names to their corresponding functions
        command_function_map = {
            "test-module": test_module,
            ### GravityZone Polling Commands ###
            "gz-poll-task-status": gz_poll_task_status_command,
            "gz-poll-investigation-activity-status": gz_poll_investigation_activity_status_command,
            "gz-poll-live-search-status": gz_poll_live_search_status_command,
            "gz-poll-endpoint-users-loggedin-status": gz_poll_endpoint_users_loggedin_status_command,
            ### GravityZone Endpoint Commands ###
            "gz-endpoint-list": gz_endpoint_list_command,
            "gz-endpoint-get": gz_endpoint_get_command,
            "gz-endpoint-users-loggedin": gz_endpoint_users_loggedin_command,
            "gz-endpoint-create-memory-dump": gz_endpoint_create_memory_dump_command,
            "gz-endpoint-memory-dump-status": gz_endpoint_memory_dump_status_command,
            "gz-endpoint-download-investigation-package": gz_endpoint_download_investigation_package_command,
            "gz-endpoint-download-file": gz_endpoint_download_file_command,
            "gz-endpoint-isolate": gz_endpoint_isolate_command,
            "gz-endpoint-deisolate": gz_endpoint_deisolate_command,
            "gz-endpoint-kill-process": gz_endpoint_kill_process_command,
            "gz-endpoint-run-command": gz_endpoint_run_command_command,
            "gz-endpoint-upload-file": gz_endpoint_upload_file_command,
            "gz-endpoint-list-by-running-process-hash": gz_endpoint_list_by_running_process_hash_command,
            "gz-endpoint-get-process-tree-by-hash": gz_endpoint_get_process_tree_by_hash_command,
            ### GravityZone Incident Commands ###
            "fetch-incidents": fetch_incidents_command,
            "gz-incident-get": gz_incident_get_command,
            "gz-incident-list": gz_incident_list_command,
            "gz-incident-add-note": gz_incident_add_note_command,
            "gz-incident-change-status": gz_incident_change_status_command,
            ### Cortex XSOAR EDL Commands ###
            "get-modified-remote-data": get_modified_remote_data_command,
            "get-remote-data": get_remote_data_command,
            "update-remote-system": update_remote_system_command,
            "get-mapping-fields": get_mapping_fields_command,
        }

        polling_functions = [
            "gz-poll-task-status",
            "gz-poll-investigation-activity-status",
            "gz-poll-live-search-status",
            "gz-poll-endpoint-users-loggedin-status",
        ]

        if command in command_function_map:
            if command in polling_functions:
                results = command_function_map[command](args, client)
            else:
                results = command_function_map[command](client, args)
            return_results(results)
        else:
            raise NotImplementedError(f"Command {command} is not implemented")
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{str(e)}")


if __name__ in ("__main__", "__builtin__", "builtins"):  # pragma: no cover
    main()