GravityZone
GravityZone provides secure access to incident and endpoint data and enables remediation actions through its APIs.
Endpoint · GravityZone
Details
| ID | GravityZone |
|---|---|
| Category | Endpoint |
| From Version | 6.10.0 |
| Docker Image | demisto/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:
- Enable Fetching incidents in your instance configuration.
-
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 settingsisFetch— Fetch incidentsmax_fetch— Maximum incidents to fetchfirst_fetch— First fetch timemirror_direction— Mirroring DirectionincidentType— Incident typeincidentFetchInterval— Incidents Fetch Interval
Commands (27)
-
fetch-incidentsRetrieves incidents from GravityZone Cloud.
-
get-mapping-fieldsReturns the list of fields available for outgoing mirroring. This command is intended for debugging purposes only.
-
get-modified-remote-dataRetrieves incidents that were modified since the last data sync. This command is intended for debugging purposes.
-
get-remote-dataRetrieves data from a remote incident without updating the current incident. This command is intended for debugging purposes.
-
gz-endpoint-create-memory-dumpInitiates 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-deisolateRestores an isolated endpoint to the network.
-
gz-endpoint-download-fileDownloads a file from an endpoint.
-
gz-endpoint-download-investigation-packageCollects and downloads an investigation package from an endpoint.
-
gz-endpoint-getRetrieves endpoint details by endpoint ID.
-
gz-endpoint-get-process-tree-by-hashRetrieves 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-isolateIsolates an endpoint from the network.
-
gz-endpoint-kill-processTerminates a process on an endpoint by process ID.
-
gz-endpoint-listRetrieves the list of managed endpoints.
-
gz-endpoint-list-by-running-process-hashRetrieves 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-statusVerifies the status of a task initiated via gz-endpoint-create-memory-dump.
-
gz-endpoint-run-commandRuns a command on the endpoint. The applied policy must have remote shell enabled for this action to work.
-
gz-endpoint-upload-fileUploads a file to an endpoint.
-
gz-endpoint-users-loggedinRetrieves details of all active user sessions on a specific managed endpoint.
-
gz-incident-add-noteAdds a note to one or more incidents.
-
gz-incident-change-statusChanges the status of one or more incidents.
-
gz-incident-getRetrieves incident details by ID.
-
gz-incident-listRetrieves incidents from the last three days for all endpoints or for a specific endpoint.
-
gz-poll-endpoint-users-loggedin-statusVerifies 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-statusChecks the status of an investigation activity. This command is not intended for direct use.
-
gz-poll-live-search-statusChecks the status of a Live Search query. This command is not intended for direct use.
-
gz-poll-task-statusChecks the task status. This command is not intended for direct use.
-
update-remote-systemUpdates 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()