GoogleSecOpsCases
Use the Google SecOps Cases integration to retrieve Cases as Incidents. This integration also provides commands to manage the Cases lifecycle.
Analytics & SIEM · Google SecOps
Details
| ID | GoogleSecOpsCases |
|---|---|
| Provider | |
| Category | Analytics & SIEM |
| From Version | 6.10.0 |
| Docker Image | demisto/googleapi-python3:1.0.0.11185775 |
| Supported Modules | Agentix XSIAM |
README
Use the Google SecOps Cases integration to retrieve Cases as Incidents. This integration also provides commands to manage the Cases lifecycle.
This integration was integrated and tested with version v1 Alpha of Google SecOps API.
Configure Google SecOps Cases in Cortex
| Parameter | Description | Required |
|---|---|---|
| User’s Service Account JSON | True | |
| API URL Format | Select the API URL format to use for API requests. Default value is ‘<chronicle>.<REGION>.<rep.googleapis.com>’. | False |
| Google SecOps Project Instance ID | Provide the Project Instance ID of the Google SecOps. Note: User can retrieve the Customer ID(Project Instance ID) in the Profile section of the Google SecOps page. |
True |
| Google SecOps Project Number | Provide the Project Number of the Google SecOps. Note: User can retrieve the Project Number in the Profile section of the Google SecOps page. If Project Number is not provided, then Project ID(from Service Account JSON) will be used. |
False |
| Region | Select the region based on the location of the Google SecOps instance. If the region is not listed in the dropdown, choose the “Other” option and specify the region in the “Other Region” text field. | True |
| Other Region | Specify the region based on the location of the Google SecOps instance. Only applicable if the “Other” option is selected in the Region dropdown. | False |
| Fetch incidents | ||
| Incident type | False | |
| First Fetch Time | The UTC date or relative timestamp from where to start fetching incidents. Default is 3 days. Note: If the value is greater than the past 7 days, it will be considered as past 7 days. The maximum is 7 days. Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 01 May 2026, 01 May 2026 04:45:33, 2026-05-17T14:05:44Z. |
False |
| How many incidents to fetch each time | The maximum number of incidents to fetch in each time. Default is 50. Note: If the value is greater than 200, it will be considered as 200. The maximum is 200. |
False |
| Case Priorities | Filter cases by priority level. Default is all. | False |
| Case Statuses | Filter cases by status. Default is all. | False |
| Case Environments | Filter cases by logical environment. | False |
| Case Tags | Filter cases by tag name. | False |
| Case Filter Logic | Logical operator to combine the case filter parameters. Default is AND. | False |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | 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.
gcb-case-list
Retrieve the list of cases.
Base Command
gcb-case-list
Input
| Argument Name | Description | Required |
|---|---|---|
| display_name | Filter the cases by display name. Supports comma-separated values. | Optional |
| priority | Filter the cases by priority. Supports comma-separated values. Possible values are: UNSPECIFIED, INFO, LOW, MEDIUM, HIGH, CRITICAL. | Optional |
| status | Filter the cases by status. Supports comma-separated values. Possible values are: OPENED, CLOSED, MERGED, CREATION_PENDING, CASE_DATA_STATE_UNSPECIFIED. | Optional |
| type | Filter the cases by type. Supports comma-separated values. Possible values are: EXTERNAL, TEST, REQUEST, CASE_TYPE_UNSPECIFIED. | Optional |
| stage | Filter the cases by stage. Supports comma-separated values. Possible values are: Triage, Incident, Investigation. | Optional |
| source | Filter the cases by source. Supports comma-separated values. Possible values are: Server, User, Simulated, Merge, AlertMove. | Optional |
| assignee | Filter the cases by assignee user email or SOC role. Supports comma-separated values. | Optional |
| environment | Filter the cases by environment assigned to the case. Supports comma-separated values. | Optional |
| tags | Filter the cases by tag names. Supports comma-separated values. | Optional |
| products | Filter the cases by product name. Supports comma-separated values. | Optional |
| important | Filter the cases by the importance flag. Possible values are: True, False. | Optional |
| incident | Filter the cases by the incident flag. Possible values are: True, False. | Optional |
| workflow_status | Filter the cases by playbook or workflow execution status. Supports comma-separated values. Possible values are: NONE, IN_PROGRESS, COMPLETED, FAILED, TERMINATED, PENDING_IN_QUEUE, PENDING_FOR_USER, WORKFLOW_STATUS_UNSPECIFIED. | Optional |
| sla | Filter the cases by SLA expiration status. Supports comma-separated values. Possible values are: OPEN_SLA, PASSED_DUE, NO_SLA, CRITICAL_EXPIRED, PAUSED, SLA_EXPIRATION_STATUS_UNSPECIFIED. | Optional |
| alerts_sla | Filter the cases by the aggregated alerts SLA expiration status. Supports comma-separated values. Possible values are: OPEN_SLA, PASSED_DUE, NO_SLA, CRITICAL_EXPIRED, PAUSED, SLA_EXPIRATION_STATUS_UNSPECIFIED. | Optional |
| create_start_time | Filter the cases created on or after this time. Note: Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 01 May 2026, 2026-05-17T14:05:44Z. |
Optional |
| create_end_time | Filter the cases created on or before this time. Note: Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 01 May 2026, 2026-05-17T14:05:44Z. |
Optional |
| update_start_time | Filter the cases updated on or after this time. Note: Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 01 May 2026, 2026-05-17T14:05:44Z. |
Optional |
| update_end_time | Filter the cases updated on or before this time. Note: Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 01 May 2026, 2026-05-17T14:05:44Z. |
Optional |
| advanced_filter | Specify a raw AIP-160 filter string for advanced conditions. When provided, all other individual filter arguments are ignored. Supported values: displayName, creatorUserId, creatorUser, assignee, stage, status, priority, important, type, environment, score, alertsSla.expirationStatus, sla.expirationStatus, tags, products, closureDetails, tasks, workflowStatus, createTime, updateTime Example: (priority=”PRIORITY_HIGH” OR status=”OPENED”) AND stage=”Investigation”. |
Optional |
| page_size | Specify the maximum number of cases to return. Note: Maximum value is 1000. Default is 50. |
Optional |
| page_token | Specify the page token for pagination. Note: Use the next_page_token from a previous gcb-case-list response. |
Optional |
| sort_by | Specify the field to sort results by. Possible values are: displayName, priority, stage, status, score, createTime, updateTime, assignee, environment, type, sla.expirationTime, alertsSla.expirationTime, sla.expirationStatus, alertsSla.expirationStatus, workflowStatus. Default is createTime. | Optional |
| sort_order | Specify the sort direction for the results. Possible values are: Asc, Desc. Default is Desc. | Optional |
| filter_logic | Specify the logical operator to combine filter conditions. Possible values are: AND, OR. Default is AND. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.name | String | The unique resource name of the Case. |
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.displayName | String | The title of the Case. |
| GoogleSecOps.Case.creatorUserId | String | The ID of the user who created the Case. |
| GoogleSecOps.Case.lastModifyingUserId | String | The ID of the user who last modified the Case. |
| GoogleSecOps.Case.createTime | String | The creation time of the Case (unix epoch milliseconds). |
| GoogleSecOps.Case.updateTime | String | The last update time of the Case (unix epoch milliseconds). |
| GoogleSecOps.Case.alertCount | Number | The number of alerts linked to the Case. |
| GoogleSecOps.Case.stage | String | The current workflow stage of the Case. |
| GoogleSecOps.Case.priority | String | The priority of the Case. |
| GoogleSecOps.Case.assignee | String | The assigned user or SOC role of the Case. |
| GoogleSecOps.Case.description | String | The description of the Case. |
| GoogleSecOps.Case.type | String | The type of the Case. |
| GoogleSecOps.Case.environment | String | The logical environment of the Case. |
| GoogleSecOps.Case.status | String | The status of the Case. |
| GoogleSecOps.Case.score | Number | The attack exposure score of the Case. |
| GoogleSecOps.Case.workflowStatus | String | The playbook or workflow status of the Case. |
| GoogleSecOps.Case.source | String | The source that created the Case. |
| GoogleSecOps.Case.important | Boolean | Whether the Case is marked as important. |
| GoogleSecOps.Case.incident | Boolean | Whether the Case is marked as an incident. |
| GoogleSecOps.Case.overflowCase | Boolean | Whether the Case is an overflow case due to large data volume. |
| GoogleSecOps.Case.involvedSuspiciousEntity | Boolean | Whether a suspicious entity is involved in the Case. |
| GoogleSecOps.Case.sla.expirationTime | String | The SLA expiration time of the Case (unix ms). |
| GoogleSecOps.Case.sla.criticalExpirationTime | String | The SLA critical expiration time of the Case (unix ms). |
| GoogleSecOps.Case.sla.expirationStatus | String | The SLA expiration status of the Case. |
| GoogleSecOps.Case.sla.remainingTimeSinceLastPause | Number | The remaining time since the last SLA pause of the Case. |
| GoogleSecOps.Case.alertsSla.expirationTime | String | The aggregated alerts SLA expiration time of the Case (unix ms). |
| GoogleSecOps.Case.alertsSla.criticalExpirationTime | String | The aggregated alerts SLA critical expiration time of the Case (unix ms). |
| GoogleSecOps.Case.alertsSla.expirationStatus | String | The aggregated alerts SLA expiration status. |
| GoogleSecOps.Case.alertsSla.remainingTimeSinceLastPause | Number | The remaining time since the last alerts SLA pause of the Case. |
| GoogleSecOps.Case.tags.displayName | String | The display name of the tag. |
| GoogleSecOps.Case.tags.priority | Number | The priority order of the tag. |
| GoogleSecOps.Case.products.displayName | String | The display name of the product. |
| GoogleSecOps.Case.products.alert | String | The resource name of the alert associated with the product. |
| GoogleSecOps.Case.tasks.id | String | The ID of the task. |
| GoogleSecOps.Case.tasks.title | String | The title of the task. |
| GoogleSecOps.Case.tasks.content | String | The content/description of the task. |
| GoogleSecOps.Case.tasks.status | String | The status of the task. |
| GoogleSecOps.Case.tasks.assignee | String | The assignee of the task. |
| GoogleSecOps.Case.tasks.author | String | The author who created the task. |
| GoogleSecOps.Case.tasks.lastAuthor | String | The last user who modified the task. |
| GoogleSecOps.Case.tasks.createTime | String | The creation time of the task (unix epoch milliseconds). |
| GoogleSecOps.Case.tasks.updateTime | String | The last update time of the task (unix epoch milliseconds). |
| GoogleSecOps.Case.tasks.caseId | Number | The ID of the Case the task belongs to. |
| GoogleSecOps.Case.tasks.favorite | Boolean | Whether the task is marked as a favorite. |
| GoogleSecOps.Case.closureDetails.reason | String | The closure reason of the Case. |
| GoogleSecOps.Case.closureDetails.comment | String | The closure comment of the Case. |
| GoogleSecOps.Case.closureDetails.rootCause | String | The root cause provided at closure. |
| GoogleSecOps.Case.closureDetails.caseClosedAction | String | The action taken when the Case was closed. |
| GoogleSecOps.PageToken.command | String | The command name associated with the pagination token. |
| GoogleSecOps.PageToken.nextPageToken | String | The token to retrieve the next page of Cases. |
| GoogleSecOps.PageToken.totalSize | Number | The total number of cases available. |
Command example
!gcb-case-list page_size=10 priority="HIGH,CRITICAL" status=OPENED stage=Triage assignee=@SOC sort_by=createTime sort_order=Desc
Context Example
{
"GoogleSecOps": {
"Case": [
{
"name": "projects/chronicle-mock-proj/locations/us/instances/00000000-0000-0000-0000-000000000001/cases/1001",
"caseId": "1001",
"displayName": "Test Case 1",
"lastModifyingUserId": "00000000-0000-0000-0000-000000000002",
"createTime": "1778778979232",
"updateTime": "1779426987796",
"alertCount": 15,
"stage": "Triage",
"priority": "PRIORITY_MEDIUM",
"assignee": "@Tier1",
"type": "EXTERNAL",
"environment": "Default Environment",
"status": "OPENED",
"workflowStatus": "PENDING_FOR_USER",
"source": "Server",
"important": false,
"incident": false,
"overflowCase": false,
"involvedSuspiciousEntity": false,
"sla": {
"expirationTime": "1780832620238",
"criticalExpirationTime": "1780580620238",
"expirationStatus": "OPEN_SLA"
},
"alertsSla": {
"expirationTime": "1782774000000",
"expirationStatus": "OPEN_SLA"
},
"tags": [
{
"displayName": "demo 1",
"priority": 0
},
{
"displayName": "demo 2",
"priority": 1
}
],
"products": [
{
"displayName": "Test Case",
"alert": "alert 1"
},
{
"displayName": "Test Case",
"alert": "alert 2"
}
],
"tasks": [
{
"id": "3",
"createTime": "1779447705049",
"updateTime": "1779447705049",
"content": "Testing",
"title": "XSOAR testing",
"author": "00000000-0000-0000-0000-000000000001",
"lastAuthor": "00000000-0000-0000-0000-000000000002",
"assignee": "00000000-0000-0000-0000-000000000001",
"status": "PENDING",
"favorite": false,
"caseId": 1001
}
],
"closureDetails": {
"reason": "NOT_MALICIOUS",
"rootCause": "False positive from detection rule",
"caseClosedAction": "MANUALLY",
"comment": "Reviewed and confirmed benign activity.\n Case closed by Siemplify API. \nAll attached playbooks and playbook blocks have been terminated.\nAll Alerts were closed."
}
},
{
"name": "projects/chronicle-mock-proj/locations/us/instances/00000000-0000-0000-0000-000000000001/cases/1002",
"caseId": "1002",
"displayName": "Test Case 2",
"lastModifyingUserId": "00000000-0000-0000-0000-000000000003",
"createTime": "1778714179967",
"updateTime": "1779416182426",
"alertCount": 5,
"stage": "Investigation",
"priority": "PRIORITY_HIGH",
"assignee": "00000000-0000-0000-0000-000000000004",
"type": "EXTERNAL",
"environment": "Production",
"status": "OPENED",
"workflowStatus": "IN_PROGRESS",
"source": "Agent",
"important": true,
"incident": false,
"overflowCase": false,
"involvedSuspiciousEntity": true,
"sla": {
"expirationStatus": "NO_SLA"
},
"alertsSla": {
"expirationStatus": "NO_SLA"
},
"tags": [
{
"displayName": "critical",
"priority": 0
}
],
"products": [
{
"displayName": "Test Product",
"alert": "alert 3"
}
]
}
],
"PageToken": {
"command": "gcb-case-list",
"nextPageToken": "next_page_token_value",
"totalSize": 2
}
}
}
Human Readable Output
Case List
Case ID Display Name Priority Status Stage Environment Workflow Status Assignee Tags SLA Alert Count Create Time 1001 Test Case 1 MEDIUM OPENED Triage Default Environment PENDING_FOR_USER @Tier1 - displayName: demo 1
priority: 0
- displayName: demo 2
priority: 1Status: OPEN_SLA
Expiration Time: 2026-06-07 11:43:40 UTC
Critical Expiration Time: 2026-06-04 13:43:40 UTC15 2026-05-14 17:16:19 UTC 1002 Test Case 2 HIGH OPENED Investigation Production IN_PROGRESS 00000000-0000-0000-0000-000000000004 - displayName: critical
priority: 0Status: NO_SLA 5 2026-05-13 23:16:19 UTC Maximum number of cases specified in page_size has been returned. To fetch the next set of cases, execute the command with the page token as
next_page_token_value.
gcb-case-get
Retrieve a specific case by its ID.
Base Command
gcb-case-get
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case. Note: Use gcb-case-list to retrieve case ID. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.name | String | The unique resource name of the Case. |
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.displayName | String | The title of the Case. |
| GoogleSecOps.Case.creatorUserId | String | The ID of the user who created the Case. |
| GoogleSecOps.Case.lastModifyingUserId | String | The ID of the user who last modified the Case. |
| GoogleSecOps.Case.createTime | String | The creation time of the Case (unix epoch milliseconds). |
| GoogleSecOps.Case.updateTime | String | The last update time of the Case (unix epoch milliseconds). |
| GoogleSecOps.Case.alertCount | Number | The number of alerts linked to the Case. |
| GoogleSecOps.Case.stage | String | The current workflow stage of the Case. |
| GoogleSecOps.Case.priority | String | The priority of the Case. |
| GoogleSecOps.Case.assignee | String | The assigned user or SOC role of the Case. |
| GoogleSecOps.Case.description | String | The description of the Case. |
| GoogleSecOps.Case.type | String | The type of the Case. |
| GoogleSecOps.Case.environment | String | The logical environment of the Case. |
| GoogleSecOps.Case.status | String | The status of the Case. |
| GoogleSecOps.Case.score | Number | The attack exposure score of the Case. |
| GoogleSecOps.Case.workflowStatus | String | The playbook or workflow status of the Case. |
| GoogleSecOps.Case.source | String | The source that created the Case. |
| GoogleSecOps.Case.important | Boolean | Whether the Case is marked as important. |
| GoogleSecOps.Case.incident | Boolean | Whether the Case is marked as an incident. |
| GoogleSecOps.Case.overflowCase | Boolean | Whether the Case is an overflow case due to large data volume. |
| GoogleSecOps.Case.involvedSuspiciousEntity | Boolean | Whether a suspicious entity is involved in the Case. |
| GoogleSecOps.Case.sla.expirationTime | String | The SLA expiration time of the Case (unix ms). |
| GoogleSecOps.Case.sla.criticalExpirationTime | String | The SLA critical expiration time of the Case (unix ms). |
| GoogleSecOps.Case.sla.expirationStatus | String | The SLA expiration status of the Case. |
| GoogleSecOps.Case.sla.remainingTimeSinceLastPause | Number | The remaining time since the last SLA pause of the Case. |
| GoogleSecOps.Case.alertsSla.expirationTime | String | The aggregated alerts SLA expiration time of the Case (unix ms). |
| GoogleSecOps.Case.alertsSla.criticalExpirationTime | String | The aggregated alerts SLA critical expiration time of the Case (unix ms). |
| GoogleSecOps.Case.alertsSla.expirationStatus | String | The aggregated alerts SLA expiration status. |
| GoogleSecOps.Case.alertsSla.remainingTimeSinceLastPause | Number | The remaining time since the last alerts SLA pause of the Case. |
| GoogleSecOps.Case.tags.displayName | String | The display name of the tag. |
| GoogleSecOps.Case.tags.priority | Number | The priority order of the tag. |
| GoogleSecOps.Case.products.displayName | String | The display name of the product. |
| GoogleSecOps.Case.products.alert | String | The resource name of the alert associated with the product. |
| GoogleSecOps.Case.tasks.id | String | The ID of the task. |
| GoogleSecOps.Case.tasks.title | String | The title of the task. |
| GoogleSecOps.Case.tasks.content | String | The content/description of the task. |
| GoogleSecOps.Case.tasks.status | String | The status of the task. |
| GoogleSecOps.Case.tasks.assignee | String | The assignee of the task. |
| GoogleSecOps.Case.tasks.author | String | The author who created the task. |
| GoogleSecOps.Case.tasks.lastAuthor | String | The last user who modified the task. |
| GoogleSecOps.Case.tasks.createTime | String | The creation time of the task (unix epoch milliseconds). |
| GoogleSecOps.Case.tasks.updateTime | String | The last update time of the task (unix epoch milliseconds). |
| GoogleSecOps.Case.tasks.caseId | Number | The ID of the Case the task belongs to. |
| GoogleSecOps.Case.tasks.favorite | Boolean | Whether the task is marked as a favorite. |
| GoogleSecOps.Case.closureDetails.reason | String | The closure reason of the Case. |
| GoogleSecOps.Case.closureDetails.comment | String | The closure comment of the Case. |
| GoogleSecOps.Case.closureDetails.rootCause | String | The root cause provided at closure. |
| GoogleSecOps.Case.closureDetails.caseClosedAction | String | The action taken when the Case was closed. |
Command example
!gcb-case-get case_id=1001
Context Example
{
"GoogleSecOps": {
"Case": {
"name": "projects/chronicle-mock-proj/locations/us/instances/00000000-0000-0000-0000-000000000001/cases/1001",
"caseId": "1001",
"displayName": "Test Case",
"lastModifyingUserId": "00000000-0000-0000-0000-000000000002",
"createTime": "1778778979232",
"updateTime": "1779426987796",
"alertCount": 15,
"stage": "Triage",
"priority": "PRIORITY_MEDIUM",
"important": false,
"incident": false,
"assignee": "@Tier1",
"type": "EXTERNAL",
"overflowCase": false,
"environment": "Default Environment",
"status": "OPENED",
"workflowStatus": "PENDING_FOR_USER",
"sla": {
"expirationTime": "1780832620238",
"criticalExpirationTime": "1780580620238",
"expirationStatus": "OPEN_SLA"
},
"alertsSla": {
"expirationTime": "1782774000000",
"expirationStatus": "OPEN_SLA"
},
"source": "Server",
"involvedSuspiciousEntity": false,
"tags": [
{"displayName": "demo 1", "priority": 0},
{"displayName": "demo 2", "priority": 1}
],
"products": [
{"displayName": "Test Case", "alert": "alert 1"},
{"displayName": "Test Case", "alert": "alert 2"}
],
"tasks": [
{
"id": "3",
"createTime": "1779447705049",
"updateTime": "1779447705049",
"content": "Testing",
"title": "XSOAR testing",
"author": "00000000-0000-0000-0000-000000000001",
"lastAuthor": "00000000-0000-0000-0000-000000000002",
"assignee": "00000000-0000-0000-0000-000000000001",
"status": "PENDING",
"favorite": false,
"caseId": 1001
}
],
"closureDetails": {
"reason": "NOT_MALICIOUS",
"rootCause": "False positive from detection rule",
"caseClosedAction": "MANUALLY",
"comment": "Reviewed and confirmed benign activity.\n Case closed by Siemplify API. \nAll attached playbooks and playbook blocks have been terminated.\nAll Alerts were closed."
}
}
}
}
Human Readable Output
Case Information
Case ID Display Name Priority Status Stage Assignee Alert Count Type Environment Source Workflow Status SLA Alerts SLA Tags Create Time Update Time Incident Important Involved Suspicious Entity Overflow Case Last Modifying User ID Products Tasks Closure Details 1001 Test Case MEDIUM OPENED Triage @Tier1 15 EXTERNAL Default Environment Server PENDING_FOR_USER Status: OPEN_SLA
Expiration Time: 2026-06-07 11:43:40 UTC
Critical Expiration Time: 2026-06-04 13:43:40 UTCStatus: OPEN_SLA
Expiration Time: 2026-06-29 23:00:00 UTC- displayName: demo 1
priority: 0
- displayName: demo 2
priority: 12026-05-14 17:16:19 UTC 2026-05-22 05:16:27 UTC False False False False 00000000-0000-0000-0000-000000000002 - displayName: Test Case
alert: alert 1
- displayName: Test Case
alert: alert 2- id: 3
createTime: 1779447705049
updateTime: 1779447705049
content: Testing
title: XSOAR testing
author: 00000000-0000-0000-0000-000000000001
lastAuthor: 00000000-0000-0000-0000-000000000002
assignee: 00000000-0000-0000-0000-000000000001
status: PENDING
favorite: False
caseId: 1001reason: NOT_MALICIOUS
rootCause: False positive from detection rule
caseClosedAction: MANUALLY
comment: Reviewed and confirmed benign activity.
Case closed by Siemplify API.
All attached playbooks and playbook blocks have been terminated.
All Alerts were closed.
gcb-case-update
Update the properties of a case.
Base Command
gcb-case-update
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case. Note: Use gcb-case-list to retrieve case ID. |
Required |
| display_name | Specify the new title for the case. Note: If the value is greater than 200 characters, it will be truncated to 200 characters. |
Optional |
| description | Specify the new description for the case. Note: If the value is greater than 1000 characters, it will be truncated to 1000 characters. |
Optional |
| important | Specify whether to mark the case as important. Possible values are: True, False. | Optional |
| incident | Specify whether to mark the case as an incident. Possible values are: True, False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.name | String | The unique resource name of the Case. |
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.displayName | String | The title of the Case. |
| GoogleSecOps.Case.creatorUserId | String | The ID of the user who created the Case. |
| GoogleSecOps.Case.lastModifyingUserId | String | The ID of the user who last modified the Case. |
| GoogleSecOps.Case.createTime | String | The creation time of the Case (unix epoch milliseconds). |
| GoogleSecOps.Case.updateTime | String | The last update time of the Case (unix epoch milliseconds). |
| GoogleSecOps.Case.alertCount | Number | The number of alerts linked to the Case. |
| GoogleSecOps.Case.stage | String | The current workflow stage of the Case. |
| GoogleSecOps.Case.priority | String | The priority of the Case. |
| GoogleSecOps.Case.assignee | String | The assigned user or SOC role of the Case. |
| GoogleSecOps.Case.description | String | The description of the Case. |
| GoogleSecOps.Case.type | String | The type of the Case. |
| GoogleSecOps.Case.environment | String | The logical environment of the Case. |
| GoogleSecOps.Case.status | String | The status of the Case. |
| GoogleSecOps.Case.score | Number | The attack exposure score of the Case. |
| GoogleSecOps.Case.workflowStatus | String | The playbook or workflow status of the Case. |
| GoogleSecOps.Case.source | String | The source that created the Case. |
| GoogleSecOps.Case.important | Boolean | Whether the Case is marked as important. |
| GoogleSecOps.Case.incident | Boolean | Whether the Case is marked as an incident. |
| GoogleSecOps.Case.overflowCase | Boolean | Whether the Case is an overflow case due to large data volume. |
| GoogleSecOps.Case.involvedSuspiciousEntity | Boolean | Whether a suspicious entity is involved in the Case. |
| GoogleSecOps.Case.sla.expirationTime | String | The SLA expiration time of the Case (unix ms). |
| GoogleSecOps.Case.sla.criticalExpirationTime | String | The SLA critical expiration time of the Case (unix ms). |
| GoogleSecOps.Case.sla.expirationStatus | String | The SLA expiration status of the Case. |
| GoogleSecOps.Case.sla.remainingTimeSinceLastPause | Number | The remaining time since the last SLA pause of the Case. |
| GoogleSecOps.Case.alertsSla.expirationTime | String | The aggregated alerts SLA expiration time of the Case (unix ms). |
| GoogleSecOps.Case.alertsSla.criticalExpirationTime | String | The aggregated alerts SLA critical expiration time of the Case (unix ms). |
| GoogleSecOps.Case.alertsSla.expirationStatus | String | The aggregated alerts SLA expiration status. |
| GoogleSecOps.Case.alertsSla.remainingTimeSinceLastPause | Number | The remaining time since the last alerts SLA pause of the Case. |
| GoogleSecOps.Case.tags.displayName | String | The display name of the tag. |
| GoogleSecOps.Case.tags.priority | Number | The priority order of the tag. |
| GoogleSecOps.Case.products.displayName | String | The display name of the product. |
| GoogleSecOps.Case.products.alert | String | The resource name of the alert associated with the product. |
| GoogleSecOps.Case.tasks.id | String | The ID of the task. |
| GoogleSecOps.Case.tasks.title | String | The title of the task. |
| GoogleSecOps.Case.tasks.content | String | The content/description of the task. |
| GoogleSecOps.Case.tasks.status | String | The status of the task. |
| GoogleSecOps.Case.tasks.assignee | String | The assignee of the task. |
| GoogleSecOps.Case.tasks.author | String | The author who created the task. |
| GoogleSecOps.Case.tasks.lastAuthor | String | The last user who modified the task. |
| GoogleSecOps.Case.tasks.createTime | String | The creation time of the task (unix epoch milliseconds). |
| GoogleSecOps.Case.tasks.updateTime | String | The last update time of the task (unix epoch milliseconds). |
| GoogleSecOps.Case.tasks.caseId | Number | The ID of the Case the task belongs to. |
| GoogleSecOps.Case.tasks.favorite | Boolean | Whether the task is marked as a favorite. |
| GoogleSecOps.Case.closureDetails.reason | String | The closure reason of the Case. |
| GoogleSecOps.Case.closureDetails.comment | String | The closure comment of the Case. |
| GoogleSecOps.Case.closureDetails.rootCause | String | The root cause provided at closure. |
| GoogleSecOps.Case.closureDetails.caseClosedAction | String | The action taken when the Case was closed. |
Command example
!gcb-case-update case_id=1001 display_name="XSOAR 1 Testing" important=True
Context Example
{
"GoogleSecOps": {
"Case": {
"caseId": "1001",
"lastModifyingUserId": "Siemplify automation",
"createTime": "1780569465745",
"updateTime": "1780912611281",
"displayName": "XSOAR 1 Testing",
"alertCount": 1,
"stage": "Triage",
"priority": "PRIORITY_LOW",
"important": true,
"incident": false,
"assignee": "00000000-0000-0000-0000-000000000001",
"type": "EXTERNAL",
"overflowCase": false,
"environment": "XSOAR",
"status": "OPENED",
"workflowStatus": "NONE",
"sla": {
"expirationStatus": "NO_SLA"
},
"alertsSla": {
"expirationStatus": "NO_SLA"
},
"source": "User",
"involvedSuspiciousEntity": false
}
}
}
Human Readable Output
Updated Case Information
Case ID Display Name Priority Status Stage Assignee Alert Count Type Environment Source Workflow Status SLA Alerts SLA Create Time Update Time Incident Important Involved Suspicious Entity Overflow Case Last Modifying User ID 1001 XSOAR 1 Testing LOW OPENED Triage 00000000-0000-0000-0000-000000000001 1 EXTERNAL XSOAR User NONE Status: NO_SLA Status: NO_SLA 2026-06-04 10:37:45 UTC 2026-06-08 09:56:51 UTC False True False False Siemplify automation
gcb-case-tag-add
Add the specified tags to the cases.
Base Command
gcb-case-tag-add
Input
| Argument Name | Description | Required |
|---|---|---|
| case_ids | Specify the IDs of the case to add tags. Supports comma-separated values. Note: Use gcb-case-list to retrieve case ID. |
Required |
| tags | Specify the tags to add to the cases. Supports comma-separated values. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.recentlyAddedTags | String | The list of recently added tags. |
Command example
!gcb-case-tag-add case_ids=1001,1002 tags=malware,phishing
Context Example
{
"GoogleSecOps": {
"Case": [
{
"caseId": "1001",
"recentlyAddedTags": ["malware", "phishing"]
},
{
"caseId": "1002",
"recentlyAddedTags": ["malware", "phishing"]
}
]
}
}
Human Readable Output
Tags malware, phishing successfully added to cases 1001, 1002.
gcb-case-tag-remove
Remove the specified tag from a case.
Base Command
gcb-case-tag-remove
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case to remove the tag. Note: Use gcb-case-list to retrieve case ID. |
Required |
| tag | Specify the tag to remove from the case. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.recentlyRemovedTag | String | The tag that was recently removed from the case. |
Command example
!gcb-case-tag-remove case_id=1001 tag=malware
Context Example
{
"GoogleSecOps": {
"Case": {
"caseId": "1001",
"recentlyRemovedTag": "malware"
}
}
}
Human Readable Output
Tag malware successfully removed from case 1001.
gcb-case-priority-change
Change the priority of the specified cases.
Base Command
gcb-case-priority-change
Input
| Argument Name | Description | Required |
|---|---|---|
| case_ids | Specify the IDs of the case to change priority. Supports comma-separated values. Note: Use gcb-case-list to retrieve case ID. |
Required |
| priority | Specify the new priority level for the cases. Possible values are: UNSPECIFIED, INFO, LOW, MEDIUM, HIGH, CRITICAL. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.priority | String | The updated priority of the Case. |
Command example
!gcb-case-priority-change case_ids=1001,1002 priority=CRITICAL
Context Example
{
"GoogleSecOps": {
"Case": [
{
"caseId": "1001",
"priority": "CRITICAL"
},
{
"caseId": "1002",
"priority": "CRITICAL"
}
]
}
}
Human Readable Output
Priority of cases 1001, 1002 successfully changed to CRITICAL.
gcb-case-stage-definition-list
Retrieve the list of case stage definitions configured in the instance.
Base Command
gcb-case-stage-definition-list
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseStageDefinition | Unknown | The list of the case stage definitions. |
Command example
#### Context Example
```json
{
"GoogleSecOps": {
"CaseStageDefinition": [
"Triage",
"Assessment",
"Investigation"
]
}
}
Human Readable Output
Case Stage Definitions: Triage, Assessment, Investigation
gcb-case-stage-change
Change the workflow stage of the specified cases.
Base Command
gcb-case-stage-change
Input
| Argument Name | Description | Required |
|---|---|---|
| case_ids | Specify the IDs of the case to change stage. Supports comma-separated values. Note: Use gcb-case-list to retrieve case ID. |
Required |
| stage | Specify the new workflow stage name. Note: Use gcb-case-stage-definition-list to retrieve case stage definition list. Possible values are: Triage, Incident, Investigation. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.stage | String | The updated workflow stage of the Case. |
Command example
!gcb-case-stage-change case_ids=1001,1002 stage=Investigation
Context Example
{
"GoogleSecOps": {
"Case": [
{
"caseId": "1001",
"stage": "Investigation"
},
{
"caseId": "1002",
"stage": "Investigation"
}
]
}
}
Human Readable Output
Stage of cases 1001, 1002 successfully changed to Investigation.
gcb-case-reopen
Reopen the specified cases.
Base Command
gcb-case-reopen
Input
| Argument Name | Description | Required |
|---|---|---|
| case_ids | Specify the IDs of the case to reopen. Supports comma-separated values. Note: Use gcb-case-list to retrieve case ID. |
Required |
| reopen_comment | Specify a comment explaining why the cases are being reopened. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.status | String | The status of the Case after reopening. |
Command example
!gcb-case-reopen case_ids=1001,1002 reopen_comment="Reopening due to new evidence of malicious activity."
Context Example
{
"GoogleSecOps": {
"Case": [
{
"caseId": "1001",
"status": "OPENED"
},
{
"caseId": "1002",
"status": "OPENED"
}
]
}
}
Human Readable Output
Cases 1001, 1002 successfully reopened.
gcb-case-close-definition-list
Retrieve the list of case close definitions configured in the instance.
Base Command
gcb-case-close-definition-list
Input
There are no input arguments for this command.
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseCloseDefinition.name | String | The unique resource name of the Case close definition. |
| GoogleSecOps.CaseCloseDefinition.closeReason | String | The close reason of the Case close definition. |
| GoogleSecOps.CaseCloseDefinition.rootCause | String | The root cause of the Case close definition. |
Command example
#### Context Example
```json
{
"GoogleSecOps": {
"CaseCloseDefinition": [
{
"name": "projects/my-project/locations/us/instances/my-instance/caseCloseDefinitions/1",
"closeReason": "MAINTENANCE",
"rootCause": "Rule under construction"
},
{
"name": "projects/my-project/locations/us/instances/my-instance/caseCloseDefinitions/2",
"closeReason": "INCONCLUSIVE",
"rootCause": "No clear conclusion"
},
{
"name": "projects/my-project/locations/us/instances/my-instance/caseCloseDefinitions/3",
"closeReason": "NOT_MALICIOUS",
"rootCause": "Similar case is already under investigation"
},
{
"name": "projects/my-project/locations/us/instances/my-instance/caseCloseDefinitions/4",
"closeReason": "MALICIOUS",
"rootCause": "Irrelevant TCP/UDP port"
}
]
}
}
Human Readable Output
Close Reason Root Cause MAINTENANCE Rule under construction INCONCLUSIVE No clear conclusion NOT_MALICIOUS Similar case is already under investigation MALICIOUS Irrelevant TCP/UDP port
gcb-case-close
Close the specified cases.
Base Command
gcb-case-close
Input
| Argument Name | Description | Required |
|---|---|---|
| case_ids | Specify the IDs of the case to close. Supports comma-separated values. Note: Use gcb-case-list to retrieve case ID. |
Required |
| close_reason | Specify the reason for closing the cases. Possible values are: MALICIOUS, NOT_MALICIOUS, MAINTENANCE, INCONCLUSIVE, UNKNOWN, CLOSE_REASON_UNSPECIFIED. | Required |
| root_cause | Specify the root cause description for the closure. | Required |
| close_comment | Specify a comment to add when closing the cases. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.status | String | The status of the Case after closure. |
| GoogleSecOps.Case.closureDetails.reason | String | The closure reason of the Case. |
| GoogleSecOps.Case.closureDetails.comment | String | The closure comment of the Case. |
| GoogleSecOps.Case.closureDetails.rootCause | String | The root cause provided at closure. |
Command example
!gcb-case-close case_ids=1001,1002 close_reason=NOT_MALICIOUS root_cause="False positive from detection rule" close_comment="Confirmed not malicious"
Context Example
{
"GoogleSecOps": {
"Case": [
{
"caseId": "1001",
"status": "CLOSED",
"closureDetails": {
"reason": "NOT_MALICIOUS",
"rootCause": "False positive",
"comment": "Confirmed not malicious"
}
},
{
"caseId": "1002",
"status": "CLOSED",
"closureDetails": {
"reason": "NOT_MALICIOUS",
"rootCause": "False positive",
"comment": "Confirmed not malicious"
}
}
]
}
}
Human Readable Output
Cases 1001, 1002 successfully closed with reason NOT_MALICIOUS.
gcb-case-assign
Assign the specified cases to a specific analyst or SOC role.
Base Command
gcb-case-assign
Input
| Argument Name | Description | Required |
|---|---|---|
| case_ids | Specify the IDs of the case to assign. Supports comma-separated values. Note: Use gcb-case-list to retrieve case ID. |
Required |
| assignee | Specify the user email address or a SOC role. For SOC roles, add the @ prefix (for example, @Tier1). |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.assignee | String | The resolved user ID or SOC role assigned to the Case after the operation. |
Command example
!gcb-case-assign case_ids=1001,1002 assignee=@SocRole
Context Example
{
"GoogleSecOps": {
"Case": [
{
"caseId": "1001",
"assignee": "@SocRole"
},
{
"caseId": "1002",
"assignee": "@SocRole"
}
]
}
}
Human Readable Output
Cases 1001, 1002 successfully assigned to @SocRole.
gcb-case-comment-list
Retrieve the list of comments associated with the specified case.
Base Command
gcb-case-comment-list
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case to retrieve comments. Note: Use gcb-case-list to retrieve case ID. |
Required |
| page_size | Specify the maximum number of comments to return. Note: Maximum value is 1000. Default is 50. |
Optional |
| page_token | Specify the page token for pagination. Note: Use the next_page_token from a previous gcb-case-comment-list response. |
Optional |
| sort_by | Specify the field to sort results by. Possible values are: createTime, updateTime, user, comment, deletionInvoker, favorite, alert, deleted. Default is createTime. | Optional |
| sort_order | Specify the sort direction for the results. Possible values are: Asc, Desc. Default is Desc. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseComment.name | String | The unique resource name of the Case comment. |
| GoogleSecOps.CaseComment.commentId | String | The ID of the Case comment. |
| GoogleSecOps.CaseComment.createTime | String | The creation time of the Case comment (unix epoch milliseconds). |
| GoogleSecOps.CaseComment.updateTime | String | The last update time of the Case comment (unix epoch milliseconds). |
| GoogleSecOps.CaseComment.user | String | User owner of the comment. |
| GoogleSecOps.CaseComment.comment | String | The text of the Case comment. |
| GoogleSecOps.CaseComment.userOwnerFullName | String | The user who created the comment. |
| GoogleSecOps.CaseComment.lastEditorFullName | String | The user who last edited the comment. |
| GoogleSecOps.CaseComment.deletedByUser | String | The user who deleted the comment. |
| GoogleSecOps.CaseComment.alertIdentifier | String | The alert associated with the comment. |
| GoogleSecOps.CaseComment.isFavorite | Boolean | Whether the comment is marked as a favorite. |
| GoogleSecOps.CaseComment.isDeleted | Boolean | Indicates if the comment has been softly deleted. |
| GoogleSecOps.CaseComment.case | String | The case associated with the comment. |
| GoogleSecOps.PageToken.command | String | The command name associated with the pagination token. |
| GoogleSecOps.PageToken.nextPageToken | String | Token to fetch the next page of case comments. |
| GoogleSecOps.PageToken.totalSize | Number | The total number of case comments available. |
Command example
!gcb-case-comment-list case_id=1001
Context Example
{
"GoogleSecOps": {
"CaseComment": [
{
"name": "projects/chronicle-mock-proj/locations/us/instances/mock-inst-uuid/cases/1001/caseComments/2001",
"commentId": "2001",
"createTime": "1780466772061",
"updateTime": "1780466772061",
"comment": "Initial assessment complete. Escalating to senior analyst.",
"isFavorite": true,
"alertIdentifier": "alert-mock-security-001",
"isDeleted": false,
"case": "1001"
},
{
"name": "projects/chronicle-mock-proj/locations/us/instances/mock-inst-uuid/cases/1001/caseComments/2002",
"commentId": "2002",
"createTime": "1780465892736",
"updateTime": "1780465892736",
"user": "user-uuid-mock-12345",
"comment": "Threat actor identified. Initiating containment measures.",
"userOwnerFullName": "Bob Wilson",
"lastEditorFullName": "Bob Wilson",
"isFavorite": false,
"isDeleted": false,
"deletedByUser": "user-uuid-mock-12345",
"case": "1001"
}
],
"PageToken": {
"command": "gcb-case-comment-list",
"nextPageToken": "mock-pagination-token",
"totalSize": 4
}
}
}
Human Readable Output
Case Comments
Author Comment Create Time Automation Initial assessment complete. Escalating to senior analyst. 2026-06-03 06:06:12 UTC Bob Wilson Threat actor identified. Initiating containment measures. 2026-06-03 05:51:32 UTC Maximum number of comments specified in page_size has been returned. To fetch the next set of comments, execute the command with the page token as
mock-pagination-token.
gcb-case-comment-create
Add a comment to the specified case.
Base Command
gcb-case-comment-create
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case to add the comment. Note: Use gcb-case-list to retrieve case ID. |
Required |
| comment | Specify the comment text to add to the case. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseComment.name | String | The unique resource name of the Case comment. |
| GoogleSecOps.CaseComment.commentId | String | The ID of the Case comment. |
| GoogleSecOps.CaseComment.createTime | String | The creation time of the Case comment (unix epoch milliseconds). |
| GoogleSecOps.CaseComment.updateTime | String | The last update time of the Case comment (unix epoch milliseconds). |
| GoogleSecOps.CaseComment.user | String | User owner of the comment. |
| GoogleSecOps.CaseComment.comment | String | The text of the Case comment. |
| GoogleSecOps.CaseComment.userOwnerFullName | String | The user who created the comment. |
| GoogleSecOps.CaseComment.lastEditorFullName | String | The user who last edited the comment. |
| GoogleSecOps.CaseComment.deletedByUser | String | The user who deleted the comment. |
| GoogleSecOps.CaseComment.alertIdentifier | String | The alert associated with the comment. |
| GoogleSecOps.CaseComment.isFavorite | Boolean | Whether the comment is marked as a favorite. |
| GoogleSecOps.CaseComment.isDeleted | Boolean | Indicates if the comment has been softly deleted. |
| GoogleSecOps.CaseComment.case | String | The case associated with the comment. |
Command example
!gcb-case-comment-create case_id=1001 comment="Investigated the outbound traffic. Confirmed malicious C2 communication."
Context Example
{
"GoogleSecOps": {
"CaseComment": {
"name": "projects/chronicle-mock-proj/locations/us/instances/mock-inst-uuid/cases/1001/caseComments/comment-001",
"commentId": "comment-001",
"createTime": "1780642722226",
"updateTime": "1780642722226",
"comment": "Investigated the outbound traffic. Confirmed malicious C2 communication.",
"isFavorite": false,
"isDeleted": false,
"case": "1001"
}
}
}
Human Readable Output
Successfully added the following comment to case “1001” at 2026-06-05 06:58:42 UTC:
Investigated the outbound traffic. Confirmed malicious C2 communication.
gcb-case-sla-pause
Pause the SLA timer for the specified case.
Base Command
gcb-case-sla-pause
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case to pause the SLA timer. Note: Use gcb-case-list to retrieve case ID. |
Required |
| message | Specify the reason for pausing the SLA timer. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.slaStatus | String | The SLA status of the Case after the operation. |
Command example
!gcb-case-sla-pause case_id=1001 message="Pausing SLA pending additional investigation."
Context Example
{
"GoogleSecOps": {
"Case": {
"caseId": "1001",
"slaStatus": "PAUSED"
}
}
}
Human Readable Output
SLA timer for case 1001 successfully paused.
gcb-case-sla-resume
Resume the SLA timer for the specified case.
Base Command
gcb-case-sla-resume
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case to resume the SLA timer. Note: Use gcb-case-list to retrieve case ID. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Case.caseId | String | The ID of the Case. |
| GoogleSecOps.Case.slaStatus | String | The SLA status of the Case after the operation. |
Command example
!gcb-case-sla-resume case_id=1001
Context Example
{
"GoogleSecOps": {
"Case": {
"caseId": "1001",
"slaStatus": "SLA_EXPIRATION_STATUS_UNSPECIFIED"
}
}
}
Human Readable Output
SLA timer for case 1001 successfully resumed.
gcb-case-alert-list
Retrieve the list of alerts associated with the specified case.
Base Command
gcb-case-alert-list
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case to list alerts. Note: Use gcb-case-list to retrieve case ID. |
Required |
| display_name | Filter alerts by display name. Supports comma-separated values. | Optional |
| priority | Filter alerts by priority. Supports comma-separated values. Possible values are: LEGACY_CASE_PRIORITY_UNSPECIFIED, UNCHANGED, INFORMATIVE, LOW, MEDIUM, HIGH, CRITICAL. | Optional |
| status | Filter alerts by status. Supports comma-separated values. Possible values are: OPEN, CLOSE, ALERT_STATUS_UNSPECIFIED. | Optional |
| product | Filter alerts by product name. Supports comma-separated values. | Optional |
| vendor | Filter alerts by vendor name. Supports comma-separated values. | Optional |
| tag | Filter alerts by tag name. Supports comma-separated values. | Optional |
| environment | Filter alerts by environment name. Supports comma-separated values. | Optional |
| source_system_name | Filter alerts by the alerting system that raised the alert. Supports comma-separated values. | Optional |
| manual | Filter alerts by whether they were created manually. Possible values are: True, False. | Optional |
| create_start_time | Filter alerts created on or after this time. Note: Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 01 May 2026, 2026-05-17T14:05:44Z. |
Optional |
| create_end_time | Filter alerts created on or before this time. Note: Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 01 May 2026, 2026-05-17T14:05:44Z. |
Optional |
| update_start_time | Filter alerts updated on or after this time. Note: Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 01 May 2026, 2026-05-17T14:05:44Z. |
Optional |
| update_end_time | Filter alerts updated on or before this time. Note: Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 01 May 2026, 2026-05-17T14:05:44Z. |
Optional |
| advanced_filter | Specify a raw AIP-160 filter string to query alerts with advanced conditions. When provided, this filter is used as-is and all other filter arguments are ignored. Note: Supported filter fields: displayName, product, vendor, environment, sourceSystemName, priority, status, createTime, updateTime. Example: (priority=’HIGH’ OR status=’OPEN’) AND product=’DLP’. |
Optional |
| filter_logic | Specify the logical operator to combine filter conditions. Possible values are: AND, OR. Default is AND. | Optional |
| page_size | Specify the maximum number of alerts to return. Note: Maximum value is 1000. Default is 50. |
Optional |
| page_token | Specify the page token for pagination. Note: Use the next_page_token from a previous gcb-case-alert-list response. |
Optional |
| sort_by | Specify the field to sort results by. Possible values are: displayName, caseId, identifier, sourceGroupingIdentifier, product, vendor, environment, ticketId, sourceSystemName, priority, status, startTime, endTime, createTime. Default is createTime. | Optional |
| sort_order | Specify the sort direction for the results. Possible values are: Asc, Desc. Default is Desc. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseAlert.name | String | The unique resource name of the Case Alert. |
| GoogleSecOps.CaseAlert.alertId | String | The ID of the Case Alert extracted from the resource name. |
| GoogleSecOps.CaseAlert.caseId | Number | The ID of the Case the alert belongs to. |
| GoogleSecOps.CaseAlert.identifier | String | The legacy identifier (alert title + GUID) of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceGroupingIdentifier | String | The source grouping identifier used to group related Case Alerts. |
| GoogleSecOps.CaseAlert.alertGroupIdentifier | String | The alert group identifier of the Case Alert. |
| GoogleSecOps.CaseAlert.displayName | String | The display name of the Case Alert. |
| GoogleSecOps.CaseAlert.product | String | The product associated with the Case Alert. |
| GoogleSecOps.CaseAlert.vendor | String | The vendor associated with the Case Alert. |
| GoogleSecOps.CaseAlert.environment | String | The environment of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceSystemName | String | The alerting system that raised the Case Alert. |
| GoogleSecOps.CaseAlert.sourceIdentifier | String | The source identifier (e.g. connector ID) of the Case Alert. |
| GoogleSecOps.CaseAlert.ruleGenerator | String | The third-party rule that triggered the Case Alert. |
| GoogleSecOps.CaseAlert.siemAlertId | String | The SIEM alert identifier of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceUrl | String | The source URL of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceRuleUrl | String | The source rule URL of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceSystemUrl | String | The source system URL of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceRuleIdentifier | String | The source rule identifier of the Case Alert. |
| GoogleSecOps.CaseAlert.ticketId | String | The ticket ID linked to the Case Alert in the integrated ticketing system. |
| GoogleSecOps.CaseAlert.additionalProperties | String | The additional connector data of the Case Alert as a JSON string. |
| GoogleSecOps.CaseAlert.priority | String | The priority of the Case Alert. |
| GoogleSecOps.CaseAlert.status | String | The status of the Case Alert. |
| GoogleSecOps.CaseAlert.manual | Boolean | Whether the Case Alert was created manually. |
| GoogleSecOps.CaseAlert.nestingDepth | Number | The nesting depth level of the Case Alert. |
| GoogleSecOps.CaseAlert.playbookStatus | String | The playbook or workflow status of the Case Alert. |
| GoogleSecOps.CaseAlert.attachedPlaybookName | String | The name of the playbook attached to the Case Alert. |
| GoogleSecOps.CaseAlert.eventCount | Number | The number of events that triggered the Case Alert. |
| GoogleSecOps.CaseAlert.playbookRunCount | Number | The number of times the first playbook was run for the Case Alert. |
| GoogleSecOps.CaseAlert.createTime | String | The creation time of the Case Alert (unix epoch milliseconds). |
| GoogleSecOps.CaseAlert.updateTime | String | The last update time of the Case Alert (unix epoch milliseconds). |
| GoogleSecOps.CaseAlert.startTime | String | The time the alert was created on the third-party system. |
| GoogleSecOps.CaseAlert.endTime | String | The time the alert was closed on the third-party system. |
| GoogleSecOps.CaseAlert.sla.expirationStatus | String | The SLA expiration status of the Case Alert. |
| GoogleSecOps.CaseAlert.sla.expirationTime | String | The SLA expiration time of the Case Alert. |
| GoogleSecOps.CaseAlert.sla.criticalExpirationTime | String | The SLA critical expiration time of the Case Alert. |
| GoogleSecOps.CaseAlert.sla.remainingTimeSinceLastPause | Number | The remaining time since the last SLA pause of the Case Alert. |
| GoogleSecOps.CaseAlert.tags.tag | String | The tag value associated with the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.reason | String | The closure reason of the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.comment | String | The closure comment of the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.rootCause | String | The root cause provided at closure of the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.closingTimeMs | String | The closure time of the Case Alert in Unix milliseconds. |
| GoogleSecOps.CaseAlert.involvedRelations.identifier | String | The identifier of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.alertIdentifier | String | The identifier of the alert the Involved Relation belongs to. |
| GoogleSecOps.CaseAlert.involvedRelations.caseId | Number | The Case ID the Involved Relation belongs to. |
| GoogleSecOps.CaseAlert.involvedRelations.relationType | String | The type of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.deviceProduct | String | The product associated with the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.deviceVendor | String | The vendor associated with the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.categoryOutcome | String | The category outcome of the Involved Relation (e.g. Blocked, Allowed). |
| GoogleSecOps.CaseAlert.involvedRelations.destinationPort | String | The destination port of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.eventClassId | String | The event display name of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.startTime | Date | The start time of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.endTime | Date | The end time of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.additionalProperties | String | The additional data of the Involved Relation as a JSON string. |
| GoogleSecOps.CaseAlert.involvedRelations.from.identifier | String | The identifier of the source entity of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.from.type | String | The type of the source entity of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.to.identifier | String | The identifier of the destination entity of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.to.type | String | The type of the destination entity of the Involved Relation. |
| GoogleSecOps.PageToken.command | String | The command name associated with the pagination token. |
| GoogleSecOps.PageToken.nextPageToken | String | The token to retrieve the next page of Case Alerts. |
| GoogleSecOps.PageToken.totalSize | Number | The total number of Case Alerts matching the query. |
Command example
!gcb-case-alert-list case_id=1001 priority=HIGH status=OPEN sort_by=priority sort_order=Asc page_size=10
Context Example
{
"GoogleSecOps": {
"CaseAlert": [
{
"name": "projects/chronicle-mock-proj/locations/us/instances/00000000-0000-0000-0000-000000000001/cases/1001/caseAlerts/1000001",
"alertId": "1000001",
"caseId": 1001,
"identifier": "TEST ALERT_00000000-0000-0000-0000-000000000010",
"displayName": "TEST ALERT",
"product": "Test Product",
"vendor": "Test Vendor",
"environment": "Default Environment",
"sourceSystemName": "Test Source System",
"ruleGenerator": "Test-Alert-Rule-01",
"sourceGroupingIdentifier": "0.0.0.1",
"sourceIdentifier": "Test Source Identifier_00000000-0000-0000-0000-000000000011",
"ticketId": "0.0.0.1_2026-05-10 08:00:00",
"additionalProperties": "{\"Name\":\"TEST ALERT\",\"Type\":\"ALERT\"}",
"priority": "HIGH",
"status": "OPEN",
"createTime": "1778778979232",
"updateTime": "1779426987796",
"startTime": "1778778979232",
"endTime": "1779426987796",
"playbookStatus": "COMPLETED",
"attachedPlaybookName": "Test Playbook",
"alertGroupIdentifier": "TEST ALERT_00000000-0000-0000-0000-000000000010",
"eventCount": 5,
"playbookRunCount": 1,
"sla": {
"expirationTime": "1780832620238",
"expirationStatus": "OPEN_SLA"
},
"tags": [
{
"tag": "demo_1"
}
],
"involvedRelations": [
{
"identifier": "00000000-0000-0000-0000-000000000020",
"alertIdentifier": "TEST ALERT_00000000-0000-0000-0000-000000000010",
"caseId": 1001,
"relationType": "ALERT",
"deviceProduct": "Test Product",
"deviceVendor": "Test Vendor",
"categoryOutcome": "Blocked",
"destinationPort": "443",
"eventClassId": "Test Event Class",
"from": {
"identifier": "0.0.0.1",
"type": "ADDRESS"
},
"to": {
"identifier": "test-host-01",
"type": "HOSTNAME"
}
}
]
},
{
"name": "projects/chronicle-mock-proj/locations/us/instances/00000000-0000-0000-0000-000000000001/cases/1001/caseAlerts/1000002",
"alertId": "1000002",
"caseId": 1001,
"identifier": "TEST ALERT 2_00000000-0000-0000-0000-000000000030",
"displayName": "TEST ALERT 2",
"product": "Test Product 2",
"vendor": "Test Vendor 2",
"environment": "Default Environment",
"sourceSystemName": "Test Source System 2",
"ruleGenerator": "Test-Alert-Rule-02",
"sourceGroupingIdentifier": "0.0.0.1",
"ticketId": "0.0.0.2_2026-05-10 07:00:00",
"additionalProperties": "{\"Name\":\"TEST ALERT 2\",\"Type\":\"ALERT\"}",
"priority": "MEDIUM",
"status": "OPEN",
"createTime": "1778778979000",
"updateTime": "1779426987000",
"startTime": "1778778979000",
"endTime": "1779426987000",
"playbookStatus": "PENDING_FOR_USER",
"attachedPlaybookName": "Test Playbook 2",
"alertGroupIdentifier": "TEST ALERT 2_00000000-0000-0000-0000-000000000030",
"eventCount": 2,
"playbookRunCount": 0,
"sla": {
"expirationTime": "1779426987000",
"expirationStatus": "OPEN_SLA"
}
}
],
"PageToken": {
"command": "gcb-case-alert-list",
"nextPageToken": "test-next-page-token",
"totalSize": 10
}
}
}
Human Readable Output
Case Alerts List
Alert ID Alert Name Create Time Priority Status Events Count Alert SLA Playbook Attached Name Playbook Attached Status 1000001 TEST ALERT 2026-05-14 17:16:19 UTC HIGH OPEN 5 Status: OPEN_SLA
Expiration Time: 2026-06-07 11:43:40 UTCTest Playbook COMPLETED 1000002 TEST ALERT 2 2026-05-14 17:16:19 UTC MEDIUM OPEN 2 Status: OPEN_SLA
Expiration Time: 2026-05-22 05:16:27 UTCTest Playbook 2 PENDING_FOR_USER Maximum number of alerts specified in page_size has been returned. To fetch the next set of alerts, execute the command with the page token as
test-next-page-token.
gcb-case-alert-get
Retrieve detailed information about a specific case alert by its ID.
Base Command
gcb-case-alert-get
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case the alert belongs to. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to retrieve. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseAlert.name | String | The unique resource name of the Case Alert. |
| GoogleSecOps.CaseAlert.alertId | String | The ID of the Case Alert extracted from the resource name. |
| GoogleSecOps.CaseAlert.caseId | Number | The ID of the Case the alert belongs to. |
| GoogleSecOps.CaseAlert.identifier | String | The legacy identifier (alert title + GUID) of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceGroupingIdentifier | String | The source grouping identifier used to group related Case Alerts. |
| GoogleSecOps.CaseAlert.alertGroupIdentifier | String | The alert group identifier of the Case Alert. |
| GoogleSecOps.CaseAlert.displayName | String | The display name of the Case Alert. |
| GoogleSecOps.CaseAlert.product | String | The product associated with the Case Alert. |
| GoogleSecOps.CaseAlert.vendor | String | The vendor associated with the Case Alert. |
| GoogleSecOps.CaseAlert.environment | String | The environment of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceSystemName | String | The alerting system that raised the Case Alert. |
| GoogleSecOps.CaseAlert.sourceIdentifier | String | The source identifier (e.g. connector ID) of the Case Alert. |
| GoogleSecOps.CaseAlert.ruleGenerator | String | The third-party rule that triggered the Case Alert. |
| GoogleSecOps.CaseAlert.siemAlertId | String | The SIEM alert identifier of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceUrl | String | The source URL of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceRuleUrl | String | The source rule URL of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceSystemUrl | String | The source system URL of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceRuleIdentifier | String | The source rule identifier of the Case Alert. |
| GoogleSecOps.CaseAlert.ticketId | String | The ticket ID linked to the Case Alert in the integrated ticketing system. |
| GoogleSecOps.CaseAlert.additionalProperties | String | The additional connector data of the Case Alert as a JSON string. |
| GoogleSecOps.CaseAlert.priority | String | The priority of the Case Alert. |
| GoogleSecOps.CaseAlert.status | String | The status of the Case Alert. |
| GoogleSecOps.CaseAlert.manual | Boolean | Whether the Case Alert was created manually. |
| GoogleSecOps.CaseAlert.nestingDepth | Number | The nesting depth level of the Case Alert. |
| GoogleSecOps.CaseAlert.playbookStatus | String | The playbook or workflow status of the Case Alert. |
| GoogleSecOps.CaseAlert.attachedPlaybookName | String | The name of the playbook attached to the Case Alert. |
| GoogleSecOps.CaseAlert.eventCount | Number | The number of events that triggered the Case Alert. |
| GoogleSecOps.CaseAlert.playbookRunCount | Number | The number of times the first playbook was run for the Case Alert. |
| GoogleSecOps.CaseAlert.createTime | String | The creation time of the Case Alert (unix epoch milliseconds). |
| GoogleSecOps.CaseAlert.updateTime | String | The last update time of the Case Alert (unix epoch milliseconds). |
| GoogleSecOps.CaseAlert.startTime | String | The time the alert was created on the third-party system. |
| GoogleSecOps.CaseAlert.endTime | String | The time the alert was closed on the third-party system. |
| GoogleSecOps.CaseAlert.sla.slaStatus | String | The SLA expiration status of the Case Alert. |
| GoogleSecOps.CaseAlert.sla.slaExpireTime | String | The SLA expiration time of the Case Alert. |
| GoogleSecOps.CaseAlert.tags.tag | String | The tag value associated with the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.reason | String | The closure reason of the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.comment | String | The closure comment of the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.rootCause | String | The root cause provided at closure of the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.closingTimeMs | String | The closure time of the Case Alert in Unix milliseconds. |
| GoogleSecOps.CaseAlert.involvedRelations.identifier | String | The identifier of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.alertIdentifier | String | The identifier of the alert the Involved Relation belongs to. |
| GoogleSecOps.CaseAlert.involvedRelations.caseId | Number | The Case ID the Involved Relation belongs to. |
| GoogleSecOps.CaseAlert.involvedRelations.relationType | String | The type of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.deviceProduct | String | The product associated with the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.deviceVendor | String | The vendor associated with the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.categoryOutcome | String | The category outcome of the Involved Relation (e.g. Blocked, Allowed). |
| GoogleSecOps.CaseAlert.involvedRelations.destinationPort | String | The destination port of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.eventClassId | String | The event display name of the Involved Relation (e.g. Email Check, Data Exfiltration). |
| GoogleSecOps.CaseAlert.involvedRelations.startTime | String | The start time of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.endTime | String | The end time of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.additionalProperties | String | The additional data of the Involved Relation as a JSON string. |
| GoogleSecOps.CaseAlert.involvedRelations.from.identifier | String | The identifier of the source entity of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.from.type | String | The type of the source entity of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.to.identifier | String | The identifier of the destination entity of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.to.type | String | The type of the destination entity of the Involved Relation. |
Command Example
!gcb-case-alert-get case_id=1001 alert_id=1000001
Context Example
{
"GoogleSecOps": {
"CaseAlert": {
"name": "projects/chronicle-mock-proj/locations/us/instances/00000000-0000-0000-0000-000000000001/cases/1001/caseAlerts/1000001",
"alertId": "1000001",
"identifier": "TEST ALERT_00000000-0000-0000-0000-000000000010",
"caseId": 1001,
"createTime": "1778778979232",
"updateTime": "1779426987796",
"ruleGenerator": "Test-Alert-Rule-01",
"sourceGroupingIdentifier": "0.0.0.1",
"product": "Test Product",
"displayName": "TEST ALERT",
"vendor": "Test Vendor",
"environment": "Default Environment",
"ticketId": "0.0.0.1_2026-05-10 08:00:00",
"sourceSystemName": "Test Source System",
"closureDetails": {
"reason": "NOT_MALICIOUS",
"comment": "Testing",
"rootCause": "False positive from detection rule",
"closingTimeMs": "1779447705049"
},
"sla": {
"expirationTime": "1780832620238",
"expirationStatus": "OPEN_SLA"
},
"priority": "HIGH",
"sourceIdentifier": "Test Source Identifier_00000000-0000-0000-0000-000000000011",
"additionalProperties": "{\"Name\":\"TEST ALERT\",\"Type\":\"ALERT\",\"EndTime\":\"1779426987796\",\"Alert_Id\":\"0.0.0.1_2026-05-10 08:00:00\",\"TicketId\":\"0.0.0.1_2026-05-10 08:00:00\",\"DisplayId\":\"0.0.0.1_2026-05-10 08:00:00\",\"StartTime\":\"1778778979232\",\"IsArtifact\":\"False\",\"IsEnriched\":\"False\",\"IsTestCase\":\"False\",\"Description\":\"Test alert description.\",\"Environment\":\"Default Environment\",\"IsSuspicious\":\"False\",\"IsVulnerable\":\"False\",\"DataAccessScope\":null,\"IsInternalAsset\":\"False\",\"IsSkipPlaybooks\":null,\"AlertBaseEventIds\":\"00000000-0000-0000-0000-000000000012\",\"EstimatedStartTime\":\"1778778979232\"}",
"status": "OPEN",
"startTime": "1778778979232",
"endTime": "1779426987796",
"playbookStatus": "COMPLETED",
"attachedPlaybookName": "Test Playbook",
"alertGroupIdentifier": "TEST ALERT_00000000-0000-0000-0000-000000000010",
"eventCount": 5,
"playbookRunCount": 1,
"tags": [
{
"tag": "demo_1"
},
{
"tag": "demo_2"
}
],
"involvedRelations": [
{
"identifier": "00000000-0000-0000-0000-000000000020",
"alertIdentifier": "TEST ALERT_00000000-0000-0000-0000-000000000010",
"caseId": 1001,
"relationType": "ALERT",
"from": {
"identifier": "0.0.0.1",
"type": "ADDRESS"
},
"to": {
"identifier": "test-host-01",
"type": "HOSTNAME"
},
"deviceProduct": "Test Product",
"deviceVendor": "Test Vendor",
"categoryOutcome": "Blocked",
"destinationPort": "443",
"eventClassId": "Test Event Class"
}
]
}
}
}
Human Readable Output
Case Alert Information
Alert ID Case ID Display Name Status Priority Product Vendor Environment Tags SLA Event Count Alert Identifier Alert Group Identifier Playbook Status Attached Playbook Name Playbook Run Count Create Time Update Time Start Time End Time Manual Rule Generator Ticket ID Source System Name Source Identifier Source Grouping Identifier Siem Alert ID Source URL Additional Properties Closure Details Involved Relations 1000001 1001 TEST ALERT OPEN HIGH Test Product Test Vendor Default Environment demo_1, demo_2 Status: OPEN_SLA
Expiration Time: 2026-06-07 11:43:40 UTC5 TEST ALERT_00000000-0000-0000-0000-000000000010 TEST ALERT_00000000-0000-0000-0000-000000000010 COMPLETED Test Playbook 1 2026-05-14 17:16:19 UTC 2026-05-22 05:16:27 UTC 2026-05-14 17:16:19 UTC 2026-05-22 05:16:27 UTC False Test-Alert-Rule-01 0.0.0.1_2026-05-10 08:00:00 Test Source System Test Source Identifier_00000000-0000-0000-0000-000000000011 0.0.0.1 0.0.0.1_2026-05-10 08:00:00 http://demo.com/alerts/1000001 {“Name”:”TEST ALERT”,”Type”:”ALERT”,”EndTime”:”1779426987796”,”Alert_Id”:”0.0.0.1_2026-05-10 08:00:00”,”TicketId”:”0.0.0.1_2026-05-10 08:00:00”,”DisplayId”:”0.0.0.1_2026-05-10 08:00:00”,”StartTime”:”1778778979232”,”IsArtifact”:”False”,”IsEnriched”:”False”,”IsTestCase”:”False”,”Description”:”Test alert description.”,”Environment”:”Default Environment”,”IsSuspicious”:”False”,”IsVulnerable”:”False”,”DataAccessScope”:null,”IsInternalAsset”:”False”,”IsSkipPlaybooks”:null,”AlertBaseEventIds”:”00000000-0000-0000-0000-000000000012”,”EstimatedStartTime”:”1778778979232”} reason: NOT_MALICIOUS
comment: Testing
rootCause: False positive from detection rule
closingTimeMs: 1779447705049- identifier: 00000000-0000-0000-0000-000000000020
alertIdentifier: TEST ALERT_00000000-0000-0000-0000-000000000010
caseId: 1001
relationType: ALERT
from:
identifier: 0.0.0.1
type: ADDRESS
to:
identifier: test-host-01
type: HOSTNAME
deviceProduct: Test Product
deviceVendor: Test Vendor
categoryOutcome: Blocked
destinationPort: 443
eventClassId: Test Event Class
gcb-case-alert-update
Update the properties of an existing case alert.
Base Command
gcb-case-alert-update
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case the alert belongs to. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to update. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| status | Specify the new status for the alert. Possible values are: ALERT_STATUS_UNSPECIFIED, OPEN, CLOSE. | Optional |
| priority | Specify the new priority for the alert. Possible values are: LEGACY_CASE_PRIORITY_UNSPECIFIED, UNCHANGED, INFORMATIVE, LOW, MEDIUM, HIGH, CRITICAL. | Optional |
| close_reason | Specify the reason for closing the alert. Note: Required when status is CLOSE. Possible values are: MALICIOUS, NOT_MALICIOUS, MAINTENANCE, INCONCLUSIVE, UNKNOWN, CLOSE_REASON_UNSPECIFIED. |
Optional |
| close_comment | Specify a comment to add when closing the alert. | Optional |
| root_cause | Specify the root cause for the alert closure. Note: Required when status is CLOSE. |
Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseAlert.name | String | The unique resource name of the Case Alert. |
| GoogleSecOps.CaseAlert.alertId | String | The ID of the Case Alert extracted from the resource name. |
| GoogleSecOps.CaseAlert.caseId | Number | The ID of the Case the alert belongs to. |
| GoogleSecOps.CaseAlert.identifier | String | The legacy identifier (alert title + GUID) of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceGroupingIdentifier | String | The source grouping identifier used to group related Case Alerts. |
| GoogleSecOps.CaseAlert.alertGroupIdentifier | String | The alert group identifier of the Case Alert. |
| GoogleSecOps.CaseAlert.displayName | String | The display name of the Case Alert. |
| GoogleSecOps.CaseAlert.product | String | The product associated with the Case Alert. |
| GoogleSecOps.CaseAlert.vendor | String | The vendor associated with the Case Alert. |
| GoogleSecOps.CaseAlert.environment | String | The environment of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceSystemName | String | The alerting system that raised the Case Alert. |
| GoogleSecOps.CaseAlert.sourceIdentifier | String | The source identifier (e.g. connector ID) of the Case Alert. |
| GoogleSecOps.CaseAlert.ruleGenerator | String | The third-party rule that triggered the Case Alert. |
| GoogleSecOps.CaseAlert.siemAlertId | String | The SIEM alert identifier of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceUrl | String | The source URL of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceRuleUrl | String | The source rule URL of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceSystemUrl | String | The source system URL of the Case Alert. |
| GoogleSecOps.CaseAlert.sourceRuleIdentifier | String | The source rule identifier of the Case Alert. |
| GoogleSecOps.CaseAlert.ticketId | String | The ticket ID linked to the Case Alert in the integrated ticketing system. |
| GoogleSecOps.CaseAlert.additionalProperties | String | The additional connector data of the Case Alert as a JSON string. |
| GoogleSecOps.CaseAlert.priority | String | The priority of the Case Alert. |
| GoogleSecOps.CaseAlert.status | String | The status of the Case Alert. |
| GoogleSecOps.CaseAlert.manual | Boolean | Whether the Case Alert was created manually. |
| GoogleSecOps.CaseAlert.nestingDepth | Number | The nesting depth level of the Case Alert. |
| GoogleSecOps.CaseAlert.playbookStatus | String | The playbook or workflow status of the Case Alert. |
| GoogleSecOps.CaseAlert.attachedPlaybookName | String | The name of the playbook attached to the Case Alert. |
| GoogleSecOps.CaseAlert.eventCount | Number | The number of events that triggered the Case Alert. |
| GoogleSecOps.CaseAlert.playbookRunCount | Number | The number of times the first playbook was run for the Case Alert. |
| GoogleSecOps.CaseAlert.createTime | String | The creation time of the Case Alert (unix epoch milliseconds). |
| GoogleSecOps.CaseAlert.updateTime | String | The last update time of the Case Alert (unix epoch milliseconds). |
| GoogleSecOps.CaseAlert.startTime | String | The time the alert was created on the third-party system. |
| GoogleSecOps.CaseAlert.endTime | String | The time the alert was closed on the third-party system. |
| GoogleSecOps.CaseAlert.sla.slaStatus | String | The SLA expiration status of the Case Alert. |
| GoogleSecOps.CaseAlert.sla.slaExpireTime | String | The SLA expiration time of the Case Alert. |
| GoogleSecOps.CaseAlert.tags.tag | String | The tag value associated with the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.reason | String | The closure reason of the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.comment | String | The closure comment of the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.rootCause | String | The root cause provided at closure of the Case Alert. |
| GoogleSecOps.CaseAlert.closureDetails.closingTimeMs | String | The closure time of the Case Alert in Unix milliseconds. |
| GoogleSecOps.CaseAlert.involvedRelations.identifier | String | The identifier of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.alertIdentifier | String | The identifier of the alert the Involved Relation belongs to. |
| GoogleSecOps.CaseAlert.involvedRelations.caseId | Number | The Case ID the Involved Relation belongs to. |
| GoogleSecOps.CaseAlert.involvedRelations.relationType | String | The type of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.deviceProduct | String | The product associated with the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.deviceVendor | String | The vendor associated with the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.categoryOutcome | String | The category outcome of the Involved Relation (e.g. Blocked, Allowed). |
| GoogleSecOps.CaseAlert.involvedRelations.destinationPort | String | The destination port of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.eventClassId | String | The event display name of the Involved Relation (e.g. Email Check, Data Exfiltration). |
| GoogleSecOps.CaseAlert.involvedRelations.startTime | String | The start time of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.endTime | String | The end time of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.additionalProperties | String | The additional data of the Involved Relation as a JSON string. |
| GoogleSecOps.CaseAlert.involvedRelations.from.identifier | String | The identifier of the source entity of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.from.type | String | The type of the source entity of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.to.identifier | String | The identifier of the destination entity of the Involved Relation. |
| GoogleSecOps.CaseAlert.involvedRelations.to.type | String | The type of the destination entity of the Involved Relation. |
Command example
!gcb-case-alert-update case_id=1001 alert_id=1000001 status=CLOSE priority=HIGH close_reason=NOT_MALICIOUS close_comment="Reviewed and confirmed as false positive." root_cause="Misconfigured DLP policy"
Context Example
{
"GoogleSecOps": {
"CaseAlert": {
"alertId": "1000001",
"identifier": "TEST ALERT_00000000-0000-0000-0000-000000000010",
"caseId": 1001,
"createTime": "1778778979232",
"updateTime": "1779426987796",
"ruleGenerator": "Test-Alert-Rule-01",
"sourceGroupingIdentifier": "0.0.0.1",
"product": "Test Product",
"displayName": "TEST ALERT",
"vendor": "Test Vendor",
"environment": "Default Environment",
"ticketId": "0.0.0.1_2026-05-10 08:00:00",
"sourceSystemName": "Test Source System",
"closureDetails": {
"reason": "NOT_MALICIOUS",
"comment": "Reviewed and confirmed as false positive.",
"rootCause": "Misconfigured DLP policy",
"closingTimeMs": "1779447705049"
},
"sla": {
"expirationTime": "1780832620238",
"expirationStatus": "OPEN_SLA"
},
"priority": "HIGH",
"additionalProperties": "{\"Name\":\"TEST ALERT\",\"Type\":\"ALERT\"}",
"status": "CLOSE",
"startTime": "1778778979232",
"endTime": "1779426987796",
"alertGroupIdentifier": "TEST ALERT_00000000-0000-0000-0000-000000000010"
}
}
}
Human Readable Output
Updated Case Alert Information
Alert ID Case ID Display Name Status Priority Product Vendor Environment SLA Alert Identifier Alert Group Identifier Create Time Update Time Start Time End Time Manual Rule Generator Ticket ID Source System Name Source Grouping Identifier Additional Properties Closure Details 1000001 1001 TEST ALERT CLOSE HIGH Test Product Test Vendor Default Environment Status: OPEN_SLA
Expiration Time: 2026-06-07 11:43:40 UTCTEST ALERT_00000000-0000-0000-0000-000000000010 TEST ALERT_00000000-0000-0000-0000-000000000010 2026-05-14 17:16:19 UTC 2026-05-22 05:16:27 UTC 2026-05-14 17:16:19 UTC 2026-05-22 05:16:27 UTC False Test-Alert-Rule-01 0.0.0.1_2026-05-10 08:00:00 Test Source System 0.0.0.1 {“Name”:”TEST ALERT”,”Type”:”ALERT”} reason: NOT_MALICIOUS
comment: Reviewed and confirmed as false positive.
rootCause: Misconfigured DLP policy
closingTimeMs: 1779447705049
gcb-case-alert-tag-add
Add a tag to a case alert.
Base Command
gcb-case-alert-tag-add
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case the alert belongs to. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to add a tag. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| tag | Specify the tag to add to the alert. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseAlert.alertId | String | The ID of the Case Alert. |
| GoogleSecOps.CaseAlert.caseId | Number | The ID of the Case the alert belongs to. |
| GoogleSecOps.CaseAlert.recentlyAddedTag | String | The recently added tag. |
Command example
!gcb-case-alert-tag-add case_id=1001 alert_id=1142656 tag=insider-threat
Context Example
{
"GoogleSecOps": {
"CaseAlert": {
"alertId": "1142656",
"caseId": 1001,
"recentlyAddedTag": "insider-threat"
}
}
}
Human Readable Output
Tag insider-threat successfully added to alert 1142656.
gcb-case-alert-tag-remove
Remove a tag from a case alert.
Base Command
gcb-case-alert-tag-remove
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case the alert belongs to. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to remove a tag. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| tag | Specify the tag to remove from the alert. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseAlert.alertId | String | The ID of the Case Alert. |
| GoogleSecOps.CaseAlert.caseId | Number | The ID of the Case the alert belongs to. |
| GoogleSecOps.CaseAlert.recentlyRemovedTag | String | The tag that was recently removed from the case alert. |
Command example
!gcb-case-alert-tag-remove case_id=1001 alert_id=1142656 tag=insider-threat
Context Example
{
"GoogleSecOps": {
"CaseAlert": {
"alertId": "1142656",
"caseId": 1001,
"recentlyRemovedTag": "insider-threat"
}
}
}
Human Readable Output
Tag insider-threat successfully removed from alert 1142656.
gcb-case-alert-move
Move a case alert to a different case.
Note: Both source and destination cases must be open.
Base Command
gcb-case-alert-move
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the source case. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to move. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| destination_case_id | Specify the destination case ID to move the alert to. Note: Use gcb-case-list to retrieve destination case ID. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseAlert.alertId | String | The ID of the Case Alert. |
| GoogleSecOps.CaseAlert.caseId | Number | The Case ID the alert was moved to. |
Command example
!gcb-case-alert-move case_id=1001 alert_id=1142656 destination_case_id=1005
Context Example
{
"GoogleSecOps": {
"CaseAlert": {
"alertId": "1142656",
"caseId": 1005
}
}
}
Human Readable Output
Successfully moved Alert
1142656to Case1005.
gcb-case-alert-sla-pause
Pause the SLA timer for the specified case alert.
Base Command
gcb-case-alert-sla-pause
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case the alert belongs to. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to pause the SLA timer. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| message | Specify the reason for pausing the SLA timer. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseAlert.alertId | String | The ID of the Case Alert. |
| GoogleSecOps.CaseAlert.caseId | Number | The ID of the Case the alert belongs to. |
| GoogleSecOps.CaseAlert.slaExpirationStatus | String | The SLA expiration status of the Case Alert after the operation. |
Command example
!gcb-case-alert-sla-pause case_id=1001 alert_id=1142656 message="Pausing SLA pending additional investigation."
Context Example
{
"GoogleSecOps": {
"CaseAlert": {
"alertId": "1142656",
"caseId": "1001",
"slaExpirationStatus": "PAUSED"
}
}
}
Human Readable Output
SLA timer for alert 1142656 successfully paused.
gcb-case-alert-sla-resume
Resume the SLA timer for the specified case alert.
Base Command
gcb-case-alert-sla-resume
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case the alert belongs to. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to resume the SLA timer. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseAlert.alertId | String | The ID of the Case Alert. |
| GoogleSecOps.CaseAlert.caseId | Number | The ID of the Case the alert belongs to. |
| GoogleSecOps.CaseAlert.slaExpirationStatus | String | The SLA expiration status of the Case Alert after the operation. |
Command example
!gcb-case-alert-sla-resume case_id=1001 alert_id=1142656
Context Example
{
"GoogleSecOps": {
"CaseAlert": {
"alertId": "1142656",
"caseId": "1001",
"slaExpirationStatus": "SLA_EXPIRATION_STATUS_UNSPECIFIED"
}
}
}
Human Readable Output
SLA timer for alert 1142656 successfully resumed.
gcb-case-alert-sla-set
Set the SLA parameters for a case alert.
Note: When critical_time is specified, total_time must be greater than critical_time.
Base Command
gcb-case-alert-sla-set
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case the alert belongs to. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to set the SLA. Note: The alert must be open. Use gcb-case-alert-list to retrieve alert ID. |
Required |
| total_time | Specify the total SLA duration. Note: Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 2 weeks, 01 May 2026, 2026-05-17T14:05:44Z. |
Required |
| critical_time | Specify the critical SLA threshold. Note: Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For example: 2 weeks, 01 May 2026, 2026-05-17T14:05:44Z. |
Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.CaseAlert.alertId | String | The ID of the Case Alert. |
| GoogleSecOps.CaseAlert.caseId | Number | The ID of the Case the alert belongs to. |
| GoogleSecOps.CaseAlert.slaExpirationTime | String | The SLA expiration time of the Case Alert (epoch ms). |
| GoogleSecOps.CaseAlert.slaCriticalExpirationTime | String | The SLA critical expiration time of the Case Alert (epoch ms). |
Command example
!gcb-case-alert-sla-set case_id=1001 alert_id=1142656 total_time="2 days" critical_time="1 day"
Context Example
{
"GoogleSecOps": {
"CaseAlert": {
"alertId": "1142656",
"caseId": "1001",
"slaExpirationTime": "1746266400000",
"slaCriticalExpirationTime": "1746180000000"
}
}
}
Human Readable Output
SLA for Alert
1142656successfully set.
gcb-case-alert-recommendation-create
Initiate an asynchronous AI recommendation for a case alert.
Base Command
gcb-case-alert-recommendation-create
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case the alert belongs to. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to generate a recommendation. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.AlertRecommendation.parent | String | The resource name of the Case Alert the recommendation belongs to. |
| GoogleSecOps.AlertRecommendation.recommendationId | String | The ID of the created recommendation. |
Command example
!gcb-case-alert-recommendation-create case_id=1001 alert_id=1000001
Context Example
{
"GoogleSecOps": {
"AlertRecommendation": {
"recommendationId": "00000000-0000-0000-0000-000000000001"
}
}
}
Human Readable Output
Successfully created the recommendation for the alert 1000001.
Recommendation ID: 00000000-0000-0000-0000-000000000001
gcb-case-alert-recommendation-fetch
Fetch a previously generated AI recommendation for a case alert.
Base Command
gcb-case-alert-recommendation-fetch
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case the alert belongs to. Note: Use gcb-case-list to retrieve case ID. |
Required |
| recommendation_id | Specify the recommendation ID returned by gcb-case-alert-recommendation-create command. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.AlertRecommendation.parent | String | The resource name of the Case Alert the recommendation belongs to. |
| GoogleSecOps.AlertRecommendation.recommendationId | String | The ID of the recommendation. |
| GoogleSecOps.AlertRecommendation.recommendation | String | The AI-generated recommendation text for the Case Alert. |
| GoogleSecOps.AlertRecommendation.alertIdentifierToCaseId | Unknown | Mapping of alert identifiers to their associated Case IDs. |
| GoogleSecOps.AlertRecommendation.marketplaceActionsTriggeredManually | Unknown | List of marketplace actions that were manually triggered on the alert. |
| GoogleSecOps.AlertRecommendation.state | String | The current state of the recommendation generation. |
Command example
!gcb-case-alert-recommendation-fetch case_id=1001 recommendation_id=00000000-0000-0000-0000-000000001000
Context Example
{
"GoogleSecOps": {
"AlertRecommendation": {
"recommendationId": "00000000-0000-0000-0000-000000001000",
"recommendation": "*Step 1: Closure Recommendation*\n\nClose the alert as \"Malicious\".",
"alertIdentifierToCaseId": {
"sample_alert_00000000-0000-0000-0000-000000000001": 100001,
"sample_alert_00000000-0000-0000-0000-000000000002": 100002
},
"marketplaceActionsTriggeredManually": [
"Enrich Web Properties",
"Enrich IPs"
],
"state": "SUCCEEDED"
}
}
}
Human Readable Output
Alert Recommendation
State: SUCCEEDED
Recommendation:
Step 1: Closure RecommendationClose the alert as “Malicious”.
Alert Identifier To Case ID:
sample_alert_00000000-0000-0000-0000-000000000001: 100001sample_alert_00000000-0000-0000-0000-000000000002: 100002
Marketplace Actions Triggered Manually: Enrich Web Properties, Enrich IPs
gcb-case-alert-customfield-list
Retrieve the list of custom field values associated with a case alert.
Base Command
gcb-case-alert-customfield-list
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case the alert belongs to. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to list custom field values. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| page_size | Specify the maximum number of custom field values to return. Note: Maximum value is 1000. Default is 50. |
Optional |
| page_token | Specify the page token for pagination. Note: Use the next_page_token from a previous gcb-case-alert-customfield-list response. |
Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.AlertCustomFieldValue.name | String | The unique resource name of the Custom Field Value. |
| GoogleSecOps.AlertCustomFieldValue.customFieldId | String | The ID of the parent Custom Field. |
| GoogleSecOps.AlertCustomFieldValue.displayName | String | The display name of the Custom Field resolved from the custom field ID. |
| GoogleSecOps.AlertCustomFieldValue.scope | String | The scope of the Custom Field Value. |
| GoogleSecOps.AlertCustomFieldValue.scopeId | String | The identifier of the scope (case or alert) this value references. |
| GoogleSecOps.AlertCustomFieldValue.values | Unknown | The value(s) for the Custom Field. |
| GoogleSecOps.AlertCustomFieldValue.valuesSearchText | String | The concatenated search text for all values of the Custom Field. |
| GoogleSecOps.PageToken.command | String | The command name for which the page token applies. |
| GoogleSecOps.PageToken.nextPageToken | String | The token to retrieve the next page of Custom Field Values. |
| GoogleSecOps.PageToken.totalSize | Number | The total number of Custom Field Values available. |
Command example
!gcb-case-alert-customfield-list case_id=1001 alert_id=2001
Context Example
{
"GoogleSecOps": {
"AlertCustomFieldValue": [
{
"customFieldId": "1",
"displayName": "Ticket Priority",
"scope": "ALERT",
"values": [
"High"
],
"valuesSearchText": "High"
},
{
"customFieldId": "2",
"displayName": "Escalation Required",
"scope": "ALERT",
"values": [
"Yes"
],
"valuesSearchText": "Yes"
},
{
"customFieldId": "3",
"displayName": "Affected Systems",
"scope": "ALERT",
"values": [
"Web Server",
"Database"
],
"valuesSearchText": "Web Server Database"
}
],
"PageToken": {
"command": "gcb-case-alert-customfield-list",
"nextPageToken": "test-next-page-token",
"totalSize": 10
}
}
}
Human Readable Output
Case Alert Custom Field Values
Custom Field ID Display Name Values Values Search Text 1 Ticket Priority High High 2 Escalation Required Yes Yes 3 Affected Systems Web Server, Database Web Server Database Maximum number of custom field values specified in page_size has been returned. To fetch the next set of custom field values, execute the command with the page token as
test-next-page-token.
gcb-case-alert-entity-list
Retrieve the list of entities associated with a case alert.
Base Command
gcb-case-alert-entity-list
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert to list entities. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| page_size | Specify the maximum number of entities to return. Note: Maximum value is 1000. Default is 50. |
Optional |
| page_token | Specify the page token for pagination. Note: Use the next_page_token from a previous gcb-case-alert-entity-list response. |
Optional |
| entity_type | Filter entities by type. Supports comma-separated values. Possible values are: ADDRESS, HOSTNAME, PROCESS, USB. | Optional |
| suspicious | Filter entities by suspicious status. Possible values are: true, false. | Optional |
| internal | Filter entities by internal status. Possible values are: true, false. | Optional |
| attacker | Filter entities by attacker designation. Possible values are: true, false. | Optional |
| pivot | Filter entities by pivot designation. Possible values are: true, false. | Optional |
| enriched | Filter entities by enrichment status. Possible values are: true, false. | Optional |
| artifact | Filter entities by artifact flag. Possible values are: true, false. | Optional |
| vulnerable | Filter entities by vulnerable flag. Possible values are: true, false. | Optional |
| manually_created | Filter entities by manually created flag. Possible values are: true, false. | Optional |
| threat_source | Filter entities by threat source. Supports comma-separated values. | Optional |
| operating_system | Filter entities by operating system. Supports comma-separated values. | Optional |
| network_title | Filter entities by network name. Supports comma-separated values. | Optional |
| network_priority | Filter entities by network priority (integer values). Supports comma-separated values. | Optional |
| environment | Filter entities by environment. Supports comma-separated values. | Optional |
| advanced_filter | Specify a raw filter expression to query entities with advanced conditions. When provided, this filter is used as-is and all other filter arguments are ignored. Note: Supported filter fields: type, suspicious, internal, attacker, pivot, enriched, artifact, vulnerable, manuallyCreated, threatSource, operatingSystem, networkTitle, networkPriority, environment, fields. Example: type=’ADDRESS’ AND suspicious=true AND internal=false. |
Optional |
| sort_by | Specify the field to sort results by. Possible values are: id, entityType, suspicious, internal, attacker, pivot, enriched, artifact, vulnerable, manuallyCreated, threatSource, operatingSystem, networkTitle, networkPriority, environment. Default is id. | Optional |
| sort_order | Specify the sort direction for the results. Possible values are: Asc, Desc. Default is Desc. | Optional |
| filter_logic | Specify the logical operator to combine filter conditions. Possible values are: AND, OR. Default is AND. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.AlertEntity.id | String | The immutable identifier of the Involved Entity. |
| GoogleSecOps.AlertEntity.identifier | String | The identifier name of the Involved Entity. |
| GoogleSecOps.AlertEntity.type | String | The type of the Involved Entity. |
| GoogleSecOps.AlertEntity.caseId | Number | The Case ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.alertIdentifier | String | The alert identifier the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.environment | String | The environment of the Involved Entity. |
| GoogleSecOps.AlertEntity.threatSource | String | The threat source associated with the Involved Entity. |
| GoogleSecOps.AlertEntity.operatingSystem | String | The operating system of the Involved Entity. |
| GoogleSecOps.AlertEntity.networkTitle | String | The network name related to the Involved Entity. |
| GoogleSecOps.AlertEntity.networkPriority | Number | The network priority of the Involved Entity. |
| GoogleSecOps.AlertEntity.entityUri | String | The full URL of the Involved Entity in the source system. |
| GoogleSecOps.AlertEntity.sourceSystemUri | String | The source system URI link of the Involved Entity. |
| GoogleSecOps.AlertEntity.additionalProperties | String | The additional properties of the Involved Entity as a JSON string. |
| GoogleSecOps.AlertEntity.suspicious | Boolean | Whether the Involved Entity is considered suspicious. |
| GoogleSecOps.AlertEntity.internal | Boolean | Whether the Involved Entity is internal. |
| GoogleSecOps.AlertEntity.attacker | Boolean | Whether the Involved Entity represents an attacker. |
| GoogleSecOps.AlertEntity.pivot | Boolean | Whether the Involved Entity is a pivot entity common to multiple cases. |
| GoogleSecOps.AlertEntity.manuallyCreated | Boolean | Whether the Involved Entity was added manually. |
| GoogleSecOps.AlertEntity.enriched | Boolean | Whether the Involved Entity has been enriched by an external action. |
| GoogleSecOps.AlertEntity.artifact | Boolean | Whether the Involved Entity is an artifact. |
| GoogleSecOps.AlertEntity.vulnerable | Boolean | Whether the Involved Entity is vulnerable. |
| GoogleSecOps.AlertEntity.fields.displayName | String | The display name of the context group of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.highlighted | Boolean | Whether the context group of the Involved Entity is highlighted. |
| GoogleSecOps.AlertEntity.fields.hidden | Boolean | Whether the context group of the Involved Entity is hidden. |
| GoogleSecOps.AlertEntity.fields.items.name | String | The property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.originalName | String | The original property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.value | String | The value of the context group item of the Involved Entity. |
| GoogleSecOps.PageToken.command | String | The command name associated with the pagination token. |
| GoogleSecOps.PageToken.nextPageToken | String | Token to fetch the next page of alert entities. |
| GoogleSecOps.PageToken.totalSize | Number | The total number of alert entities available. |
Command example
!gcb-case-alert-entity-list case_id=306082 alert_id=1001
Context Example
{
"GoogleSecOps": {
"AlertEntity": [
{
"id": "376359",
"type": "ADDRESS",
"suspicious": true,
"internal": true,
"networkPriority": 0,
"attacker": false,
"pivot": false,
"environment": "Default Environment",
"manuallyCreated": false,
"additionalProperties": "{\"Type\":\"ADDRESS\",\"IsArtifact\":\"False\",\"IsEnriched\":\"False\",\"IsVulnerable\":\"False\",\"IsInternalAsset\":\"True\",\"OriginalIdentifier\":\"999.999.999.999\"}",
"enriched": false,
"artifact": false,
"vulnerable": false,
"entityUri": "/entity?indicator=999.999.999.999&type=ipAddress",
"fields": [
{
"displayName": "Default",
"highlighted": false,
"hidden": false,
"items": [
{"name": "Network_Priority", "originalName": "Network_Priority", "value": "0"},
{"name": "IsSuspicious", "originalName": "IsSuspicious", "value": "True"}
]
},
{
"displayName": "Entity",
"highlighted": false,
"hidden": false,
"items": [
{"name": "Is Pivot", "originalName": "IsPivot", "value": "False"}
]
}
],
"alertIdentifier": "Testing Alert_62923d4f-05f0-47bf-9290-d0c94406e775",
"caseId": 306082,
"identifier": "999.999.999.999"
},
{
"id": "376358",
"type": "ADDRESS",
"suspicious": false,
"internal": false,
"networkPriority": 0,
"attacker": false,
"pivot": false,
"environment": "Default Environment",
"manuallyCreated": false,
"additionalProperties": "{\"Type\":\"ADDRESS\",\"IsArtifact\":\"False\",\"IsEnriched\":\"True\",\"Censys_ports\":\"53, 443, 853\",\"IsVulnerable\":\"False\",\"IsInternalAsset\":\"False\",\"OriginalIdentifier\":\"8.8.8.8\"}",
"enriched": true,
"artifact": false,
"vulnerable": false,
"entityUri": "/entity?indicator=8.8.8.8&type=ipAddress",
"fields": [
{
"displayName": "Default",
"highlighted": false,
"hidden": false,
"items": [
{"name": "Network_Priority", "originalName": "Network_Priority", "value": "0"},
{"name": "IsSuspicious", "originalName": "IsSuspicious", "value": "False"}
]
},
{
"displayName": "Entity",
"highlighted": false,
"hidden": false,
"items": [
{"name": "Is Pivot", "originalName": "IsPivot", "value": "False"}
]
}
],
"alertIdentifier": "Testing Alert_62923d4f-05f0-47bf-9290-d0c94406e775",
"caseId": 306082,
"identifier": "8.8.8.8"
}
]
}
}
Human Readable Output
Alert Entities List
ID Identifier Type Environment Suspicious Internal Attacker Pivot Enriched Artifact Vulnerable Manually Created Network Priority Entity URI 376359 999.999.999.999 ADDRESS Default Environment True True False False False False False False 0 /entity?indicator=999.999.999.999&type=ipAddress 376358 8.8.8.8 ADDRESS Default Environment False False False False True False False False 0 /entity?indicator=8.8.8.8&type=ipAddress
gcb-case-alert-entity-get
Retrieve detailed information about a specific involved entity in a case alert.
Base Command
gcb-case-alert-entity-get
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| entity_id | Specify the ID of the involved entity to retrieve. Note: Use gcb-case-alert-entity-list to retrieve entity ID. |
Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.AlertEntity.id | String | The immutable identifier of the Involved Entity. |
| GoogleSecOps.AlertEntity.identifier | String | The identifier name of the Involved Entity. |
| GoogleSecOps.AlertEntity.type | String | The type of the Involved Entity. |
| GoogleSecOps.AlertEntity.alertId | String | The Alert ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.caseId | Number | The Case ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.alertIdentifier | String | The alert identifier the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.environment | String | The environment of the Involved Entity. |
| GoogleSecOps.AlertEntity.threatSource | String | The threat source associated with the Involved Entity. |
| GoogleSecOps.AlertEntity.operatingSystem | String | The operating system of the Involved Entity. |
| GoogleSecOps.AlertEntity.networkTitle | String | The network name related to the Involved Entity. |
| GoogleSecOps.AlertEntity.networkPriority | Number | The network priority of the Involved Entity. |
| GoogleSecOps.AlertEntity.entityUri | String | The full URL of the Involved Entity in the source system. |
| GoogleSecOps.AlertEntity.sourceSystemUri | String | The source system URI link of the Involved Entity. |
| GoogleSecOps.AlertEntity.additionalProperties | String | The additional properties of the Involved Entity as a JSON string. |
| GoogleSecOps.AlertEntity.suspicious | Boolean | Whether the Involved Entity is considered suspicious. |
| GoogleSecOps.AlertEntity.internal | Boolean | Whether the Involved Entity is internal. |
| GoogleSecOps.AlertEntity.attacker | Boolean | Whether the Involved Entity represents an attacker. |
| GoogleSecOps.AlertEntity.pivot | Boolean | Whether the Involved Entity is a pivot entity common to multiple cases. |
| GoogleSecOps.AlertEntity.manuallyCreated | Boolean | Whether the Involved Entity was added manually. |
| GoogleSecOps.AlertEntity.enriched | Boolean | Whether the Involved Entity has been enriched by an external action. |
| GoogleSecOps.AlertEntity.artifact | Boolean | Whether the Involved Entity is an artifact. |
| GoogleSecOps.AlertEntity.vulnerable | Boolean | Whether the Involved Entity is vulnerable. |
| GoogleSecOps.AlertEntity.fields.displayName | String | The display name of the context group of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.highlighted | Boolean | Whether the context group of the Involved Entity is highlighted. |
| GoogleSecOps.AlertEntity.fields.hidden | Boolean | Whether the context group of the Involved Entity is hidden. |
| GoogleSecOps.AlertEntity.fields.items.name | String | The property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.originalName | String | The original property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.value | String | The value of the context group item of the Involved Entity. |
Command example
!gcb-case-alert-entity-get case_id=306082 alert_id=1001 entity_id=376359
Context Example
{
"GoogleSecOps": {
"AlertEntity": {
"id": "376359",
"type": "ADDRESS",
"suspicious": false,
"internal": false,
"networkPriority": 0,
"attacker": false,
"pivot": false,
"environment": "Default Environment",
"manuallyCreated": false,
"additionalProperties": "{\"Type\":\"ADDRESS\",\"IsArtifact\":\"False\",\"IsEnriched\":\"False\",\"IsVulnerable\":\"False\",\"IsInternalAsset\":\"False\",\"OriginalIdentifier\":\"999.999.999.999\"}",
"enriched": false,
"artifact": false,
"vulnerable": false,
"entityUri": "/entity?indicator=999.999.999.999&type=ipAddress",
"fields": [
{
"displayName": "Default",
"highlighted": false,
"hidden": false,
"items": [
{
"name": "Network_Priority",
"originalName": "Network_Priority",
"value": "0"
},
{
"name": "IsSuspicious",
"originalName": "IsSuspicious",
"value": "False"
},
{
"name": "IsAttacker",
"originalName": "IsAttacker",
"value": "False"
},
{
"name": "IsManuallyCreated",
"originalName": "IsManuallyCreated",
"value": "False"
},
{
"name": "Environment",
"originalName": "Environment",
"value": "Default Environment"
},
{
"name": "Alert_Id",
"originalName": "Alert_Id",
"value": "Testing Alert_62923d4f-05f0-47bf-9290-d0c94406e775"
},
{
"name": "Type",
"originalName": "Type",
"value": "ADDRESS"
},
{
"name": "IsArtifact",
"originalName": "IsArtifact",
"value": "False"
},
{
"name": "IsEnriched",
"originalName": "IsEnriched",
"value": "False"
},
{
"name": "IsVulnerable",
"originalName": "IsVulnerable",
"value": "False"
},
{
"name": "IsInternalAsset",
"originalName": "IsInternalAsset",
"value": "False"
},
{
"name": "OriginalIdentifier",
"originalName": "OriginalIdentifier",
"value": "999.999.999.999"
}
]
},
{
"displayName": "Entity",
"highlighted": false,
"hidden": false,
"items": [
{
"name": "Is Pivot",
"originalName": "IsPivot",
"value": "False"
}
]
}
],
"alertIdentifier": "Testing Alert_62923d4f-05f0-47bf-9290-d0c94406e775",
"caseId": 306082,
"alertId": "1001",
"identifier": "999.999.999.999"
}
}
}
Human Readable Output
Entity Information
ID Identifier Alert ID Case ID Alert Identifier Type Environment Suspicious Internal Attacker Pivot Enriched Artifact Vulnerable Manually Created Network Priority Entity URI Additional Properties Fields 376359 999.999.999.999 1001 306082 Testing Alert_62923d4f-05f0-47bf-9290-d0c94406e775 ADDRESS Default Environment False False False False False False False False 0 /entity?indicator=999.999.999.999&type=ipAddress {“Type”:”ADDRESS”,”IsArtifact”:”False”,”IsEnriched”:”False”,”IsVulnerable”:”False”,”IsInternalAsset”:”False”,”OriginalIdentifier”:”999.999.999.999”} - displayName: Default
highlighted: False
hidden: False
items:
- name: Network_Priority
originalName: Network_Priority
value: 0
- name: IsSuspicious
originalName: IsSuspicious
value: False
- name: IsAttacker
originalName: IsAttacker
value: False
- name: IsManuallyCreated
originalName: IsManuallyCreated
value: False
- name: Environment
originalName: Environment
value: Default Environment
- name: Alert_Id
originalName: Alert_Id
value: Testing Alert_62923d4f-05f0-47bf-9290-d0c94406e775
- name: Type
originalName: Type
value: ADDRESS
- name: IsArtifact
originalName: IsArtifact
value: False
- name: IsEnriched
originalName: IsEnriched
value: False
- name: IsVulnerable
originalName: IsVulnerable
value: False
- name: IsInternalAsset
originalName: IsInternalAsset
value: False
- name: OriginalIdentifier
originalName: OriginalIdentifier
value: 999.999.999.999
- displayName: Entity
highlighted: False
hidden: False
items:
- name: Is Pivot
originalName: IsPivot
value: False
gcb-case-alert-entity-create
Manually create a new involved entity within a case alert.
Base Command
gcb-case-alert-entity-create
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| identifier | Specify the identifier name of the entity (e.g. IP address, hostname value, process name). | Required |
| entity_type | Specify the type of the new entity. Note: Immutable after creation. Possible values are: ADDRESS, HOSTNAME, PROCESS, USB. |
Required |
| suspicious | Specify whether the entity is suspicious. Possible values are: true, false. Default is false. | Optional |
| internal | Specify whether the entity is internal to the organization. Possible values are: true, false. Default is false. | Optional |
| attacker | Specify whether the entity represents an attacker. Possible values are: true, false. | Optional |
| pivot | Specify whether the entity is a pivot entity common to multiple cases. Possible values are: true, false. | Optional |
| operating_system | Specify the operating system of the entity. | Optional |
| network_title | Specify the network name related to the entity. | Optional |
| threat_source | Specify the threat source name associated with the entity. | Optional |
| network_priority | Specify the network priority of the entity (non-negative integer). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.AlertEntity.id | String | The immutable identifier of the Involved Entity. |
| GoogleSecOps.AlertEntity.identifier | String | The identifier name of the Involved Entity. |
| GoogleSecOps.AlertEntity.type | String | The type of the Involved Entity. |
| GoogleSecOps.AlertEntity.alertId | String | The Alert ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.caseId | Number | The Case ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.alertIdentifier | String | The alert identifier the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.environment | String | The environment of the Involved Entity. |
| GoogleSecOps.AlertEntity.threatSource | String | The threat source associated with the Involved Entity. |
| GoogleSecOps.AlertEntity.operatingSystem | String | The operating system of the Involved Entity. |
| GoogleSecOps.AlertEntity.networkTitle | String | The network name related to the Involved Entity. |
| GoogleSecOps.AlertEntity.networkPriority | Number | The network priority of the Involved Entity. |
| GoogleSecOps.AlertEntity.entityUri | String | The full URL of the Involved Entity in the source system. |
| GoogleSecOps.AlertEntity.sourceSystemUri | String | The source system URI link of the Involved Entity. |
| GoogleSecOps.AlertEntity.additionalProperties | String | The additional properties of the Involved Entity as a JSON string. |
| GoogleSecOps.AlertEntity.suspicious | Boolean | Whether the Involved Entity is considered suspicious. |
| GoogleSecOps.AlertEntity.internal | Boolean | Whether the Involved Entity is internal. |
| GoogleSecOps.AlertEntity.attacker | Boolean | Whether the Involved Entity represents an attacker. |
| GoogleSecOps.AlertEntity.pivot | Boolean | Whether the Involved Entity is a pivot entity common to multiple cases. |
| GoogleSecOps.AlertEntity.manuallyCreated | Boolean | Whether the Involved Entity was added manually. |
| GoogleSecOps.AlertEntity.enriched | Boolean | Whether the Involved Entity has been enriched by an external action. |
| GoogleSecOps.AlertEntity.artifact | Boolean | Whether the Involved Entity is an artifact. |
| GoogleSecOps.AlertEntity.vulnerable | Boolean | Whether the Involved Entity is vulnerable. |
| GoogleSecOps.AlertEntity.fields.displayName | String | The display name of the context group of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.highlighted | Boolean | Whether the context group of the Involved Entity is highlighted. |
| GoogleSecOps.AlertEntity.fields.hidden | Boolean | Whether the context group of the Involved Entity is hidden. |
| GoogleSecOps.AlertEntity.fields.items.name | String | The property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.originalName | String | The original property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.value | String | The value of the context group item of the Involved Entity. |
Command example
!gcb-case-alert-entity-create case_id=123456 alert_id=1001 identifier=192.0.2.1 entity_type=ADDRESS suspicious=true
Context Example
{
"GoogleSecOps": {
"AlertEntity": {
"id": "111111",
"type": "ADDRESS",
"suspicious": true,
"internal": false,
"networkPriority": 0,
"attacker": false,
"pivot": false,
"environment": "Default Environment",
"manuallyCreated": true,
"additionalProperties": "{\"Type\":\"ADDRESS\",\"IsArtifact\":\"False\",\"IsEnriched\":\"False\",\"IsVulnerable\":\"False\",\"IsInternalAsset\":\"False\",\"OriginalIdentifier\":\"192.0.2.1\"}",
"enriched": false,
"artifact": false,
"vulnerable": false,
"fields": [
{
"displayName": "Default",
"highlighted": false,
"hidden": false,
"items": [
{
"name": "Network_Priority",
"originalName": "Network_Priority",
"value": "0"
},
{
"name": "IsSuspicious",
"originalName": "IsSuspicious",
"value": "True"
},
{
"name": "IsAttacker",
"originalName": "IsAttacker",
"value": "False"
},
{
"name": "IsManuallyCreated",
"originalName": "IsManuallyCreated",
"value": "True"
},
{
"name": "Environment",
"originalName": "Environment",
"value": "Default Environment"
},
{
"name": "Alert_Id",
"originalName": "Alert_Id",
"value": "Testing Alert_00000000-0000-0000-0000-000000000001"
}
]
},
{
"displayName": "Entity",
"highlighted": false,
"hidden": false,
"items": [
{
"name": "Is Pivot",
"originalName": "IsPivot",
"value": "False"
}
]
}
],
"alertIdentifier": "Testing Alert_00000000-0000-0000-0000-000000000001",
"caseId": 123456,
"alertId": "1001",
"identifier": "192.0.2.1"
}
}
}
Human Readable Output
Entity Information
ID Identifier Alert ID Case ID Alert Identifier Type Environment Suspicious Internal Attacker Pivot Enriched Artifact Vulnerable Manually Created Network Priority Additional Properties Fields 111111 192.0.2.1 1001 123456 Testing Alert_00000000-0000-0000-0000-000000000001 ADDRESS Default Environment True False False False False False False True 0 {“Type”:”ADDRESS”,”IsArtifact”:”False”,”IsEnriched”:”False”,”IsVulnerable”:”False”,”IsInternalAsset”:”False”,”OriginalIdentifier”:”192.0.2.1”} - displayName: Default
highlighted: False
hidden: False
items:
- name: Network_Priority
originalName: Network_Priority
value: 0
- name: IsSuspicious
originalName: IsSuspicious
value: True
- name: IsAttacker
originalName: IsAttacker
value: False
- name: IsManuallyCreated
originalName: IsManuallyCreated
value: True
- name: Environment
originalName: Environment
value: Default Environment
- name: Alert_Id
originalName: Alert_Id
value: Testing Alert_00000000-0000-0000-0000-000000000001
- displayName: Entity
highlighted: False
hidden: False
items:
- name: Is Pivot
originalName: IsPivot
value: False
gcb-case-alert-entity-update
Update the attributes of an existing involved entity in a case alert.
Base Command
gcb-case-alert-entity-update
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| entity_id | Specify the ID of the involved entity to update. Note: Use gcb-case-alert-entity-list to retrieve entity ID. |
Required |
| suspicious | Specify the updated suspicion flag for the entity. Possible values are: true, false. | Optional |
| internal | Specify the updated internal flag for the entity. Possible values are: true, false. | Optional |
| attacker | Specify the updated attacker designation for the entity. Possible values are: true, false. | Optional |
| pivot | Specify the updated pivot designation for the entity. Possible values are: true, false. | Optional |
| operating_system | Specify the updated operating system of the entity. | Optional |
| network_title | Specify the updated network name related to the entity. | Optional |
| threat_source | Specify the updated threat source associated with the entity. | Optional |
| network_priority | Specify the updated network priority associated with the entity (non-negative integer). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.AlertEntity.id | String | The immutable identifier of the Involved Entity. |
| GoogleSecOps.AlertEntity.identifier | String | The identifier name of the Involved Entity. |
| GoogleSecOps.AlertEntity.type | String | The type of the Involved Entity. |
| GoogleSecOps.AlertEntity.alertId | String | The Alert ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.caseId | Number | The Case ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.alertIdentifier | String | The alert identifier the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.environment | String | The environment of the Involved Entity. |
| GoogleSecOps.AlertEntity.threatSource | String | The threat source associated with the Involved Entity. |
| GoogleSecOps.AlertEntity.operatingSystem | String | The operating system of the Involved Entity. |
| GoogleSecOps.AlertEntity.networkTitle | String | The network name related to the Involved Entity. |
| GoogleSecOps.AlertEntity.networkPriority | Number | The network priority of the Involved Entity. |
| GoogleSecOps.AlertEntity.entityUri | String | The full URL of the Involved Entity in the source system. |
| GoogleSecOps.AlertEntity.sourceSystemUri | String | The source system URI link of the Involved Entity. |
| GoogleSecOps.AlertEntity.additionalProperties | String | The additional properties of the Involved Entity as a JSON string. |
| GoogleSecOps.AlertEntity.suspicious | Boolean | Whether the Involved Entity is considered suspicious. |
| GoogleSecOps.AlertEntity.internal | Boolean | Whether the Involved Entity is internal. |
| GoogleSecOps.AlertEntity.attacker | Boolean | Whether the Involved Entity represents an attacker. |
| GoogleSecOps.AlertEntity.pivot | Boolean | Whether the Involved Entity is a pivot entity common to multiple cases. |
| GoogleSecOps.AlertEntity.manuallyCreated | Boolean | Whether the Involved Entity was added manually. |
| GoogleSecOps.AlertEntity.enriched | Boolean | Whether the Involved Entity has been enriched by an external action. |
| GoogleSecOps.AlertEntity.artifact | Boolean | Whether the Involved Entity is an artifact. |
| GoogleSecOps.AlertEntity.vulnerable | Boolean | Whether the Involved Entity is vulnerable. |
| GoogleSecOps.AlertEntity.fields.displayName | String | The display name of the context group of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.highlighted | Boolean | Whether the context group of the Involved Entity is highlighted. |
| GoogleSecOps.AlertEntity.fields.hidden | Boolean | Whether the context group of the Involved Entity is hidden. |
| GoogleSecOps.AlertEntity.fields.items.name | String | The property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.originalName | String | The original property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.value | String | The value of the context group item of the Involved Entity. |
Command example
!gcb-case-alert-entity-update case_id=306082 alert_id=1001 entity_id=376398 suspicious=true attacker=true network_priority=5
Context Example
{
"GoogleSecOps": {
"AlertEntity": {
"id": "376398",
"type": "ADDRESS",
"suspicious": true,
"internal": false,
"networkPriority": 5,
"attacker": true,
"pivot": false,
"environment": "Default Environment",
"manuallyCreated": true,
"additionalProperties": "{\"Type\":\"ADDRESS\",\"IsArtifact\":\"False\",\"IsEnriched\":\"False\",\"IsVulnerable\":\"False\",\"IsInternalAsset\":\"False\",\"OriginalIdentifier\":\"192.0.2.1\"}",
"enriched": false,
"artifact": false,
"vulnerable": false,
"fields": [
{
"displayName": "Default",
"highlighted": false,
"hidden": false,
"items": [
{
"name": "Network_Priority",
"originalName": "Network_Priority",
"value": "5"
},
{
"name": "IsSuspicious",
"originalName": "IsSuspicious",
"value": "True"
},
{
"name": "IsAttacker",
"originalName": "IsAttacker",
"value": "True"
},
{
"name": "IsManuallyCreated",
"originalName": "IsManuallyCreated",
"value": "True"
},
{
"name": "Environment",
"originalName": "Environment",
"value": "Default Environment"
},
{
"name": "Alert_Id",
"originalName": "Alert_Id",
"value": "Testing Alert_00000000-0000-0000-0000-000000000001"
}
]
},
{
"displayName": "Entity",
"highlighted": false,
"hidden": false,
"items": [
{
"name": "Is Pivot",
"originalName": "IsPivot",
"value": "False"
}
]
}
],
"alertIdentifier": "Testing Alert_00000000-0000-0000-0000-000000000001",
"caseId": 306082,
"alertId": "1001",
"identifier": "192.0.2.1"
}
}
}
Human Readable Output
Updated Entity Information
ID Identifier Alert ID Case ID Alert Identifier Type Environment Suspicious Internal Attacker Pivot Enriched Artifact Vulnerable Manually Created Network Priority Additional Properties Fields 376398 192.0.2.1 1001 306082 Testing Alert_00000000-0000-0000-0000-000000000001 ADDRESS Default Environment True False True False False False False True 5 {“Type”:”ADDRESS”,”IsArtifact”:”False”,”IsEnriched”:”False”,”IsVulnerable”:”False”,”IsInternalAsset”:”False”,”OriginalIdentifier”:”192.0.2.1”} - displayName: Default
highlighted: False
hidden: False
items:
- name: Network_Priority
originalName: Network_Priority
value: 5
- name: IsSuspicious
originalName: IsSuspicious
value: True
- name: IsAttacker
originalName: IsAttacker
value: True
- name: IsManuallyCreated
originalName: IsManuallyCreated
value: True
- name: Environment
originalName: Environment
value: Default Environment
- name: Alert_Id
originalName: Alert_Id
value: Testing Alert_00000000-0000-0000-0000-000000000001
- displayName: Entity
highlighted: False
hidden: False
items:
- name: Is Pivot
originalName: IsPivot
value: False
gcb-case-alert-entity-property-add
Add a new custom property to an involved entity in a case alert.
Base Command
gcb-case-alert-entity-property-add
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| entity_id | Specify the ID of the involved entity to add a property. Note: Use gcb-case-alert-entity-list to retrieve entity ID. |
Required |
| key | Specify the property key to add. Note: The key must not already exist on the entity. |
Required |
| value | Specify the value for the new property. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.AlertEntity.id | String | The immutable identifier of the Involved Entity. |
| GoogleSecOps.AlertEntity.identifier | String | The identifier name of the Involved Entity. |
| GoogleSecOps.AlertEntity.type | String | The type of the Involved Entity. |
| GoogleSecOps.AlertEntity.alertId | String | The Alert ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.caseId | Number | The Case ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.alertIdentifier | String | The alert identifier the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.environment | String | The environment of the Involved Entity. |
| GoogleSecOps.AlertEntity.threatSource | String | The threat source associated with the Involved Entity. |
| GoogleSecOps.AlertEntity.operatingSystem | String | The operating system of the Involved Entity. |
| GoogleSecOps.AlertEntity.networkTitle | String | The network name related to the Involved Entity. |
| GoogleSecOps.AlertEntity.networkPriority | Number | The network priority of the Involved Entity. |
| GoogleSecOps.AlertEntity.entityUri | String | The full URL of the Involved Entity in the source system. |
| GoogleSecOps.AlertEntity.sourceSystemUri | String | The source system URI link of the Involved Entity. |
| GoogleSecOps.AlertEntity.additionalProperties | String | The additional properties of the Involved Entity as a JSON string. |
| GoogleSecOps.AlertEntity.suspicious | Boolean | Whether the Involved Entity is considered suspicious. |
| GoogleSecOps.AlertEntity.internal | Boolean | Whether the Involved Entity is internal. |
| GoogleSecOps.AlertEntity.attacker | Boolean | Whether the Involved Entity represents an attacker. |
| GoogleSecOps.AlertEntity.pivot | Boolean | Whether the Involved Entity is a pivot entity common to multiple cases. |
| GoogleSecOps.AlertEntity.manuallyCreated | Boolean | Whether the Involved Entity was added manually. |
| GoogleSecOps.AlertEntity.enriched | Boolean | Whether the Involved Entity has been enriched by an external action. |
| GoogleSecOps.AlertEntity.artifact | Boolean | Whether the Involved Entity is an artifact. |
| GoogleSecOps.AlertEntity.vulnerable | Boolean | Whether the Involved Entity is vulnerable. |
| GoogleSecOps.AlertEntity.fields.displayName | String | The display name of the context group of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.highlighted | Boolean | Whether the context group of the Involved Entity is highlighted. |
| GoogleSecOps.AlertEntity.fields.hidden | Boolean | Whether the context group of the Involved Entity is hidden. |
| GoogleSecOps.AlertEntity.fields.items.name | String | The property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.originalName | String | The original property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.value | String | The value of the context group item of the Involved Entity. |
Command example
!gcb-case-alert-entity-property-add case_id=306082 alert_id=1001 entity_id=376398 key=total_score value=42/72
Context Example
{
"GoogleSecOps": {
"AlertEntity": {
"id": "376398",
"type": "ADDRESS",
"suspicious": true,
"internal": false,
"networkPriority": 5,
"attacker": true,
"pivot": false,
"environment": "Default Environment",
"manuallyCreated": true,
"additionalProperties": "{\"Type\":\"ADDRESS\",\"IsArtifact\":\"False\",\"IsEnriched\":\"False\",\"IsVulnerable\":\"False\",\"IsInternalAsset\":\"False\",\"OriginalIdentifier\":\"192.0.2.1\"}",
"enriched": true,
"artifact": false,
"vulnerable": false,
"fields": [
{
"displayName": "Default",
"highlighted": false,
"hidden": false,
"items": [
{"name": "Network_Priority", "originalName": "Network_Priority", "value": "5"},
{"name": "IsSuspicious", "originalName": "IsSuspicious", "value": "True"},
{"name": "IsAttacker", "originalName": "IsAttacker", "value": "True"},
{"name": "IsManuallyCreated", "originalName": "IsManuallyCreated", "value": "True"},
{"name": "Environment", "originalName": "Environment", "value": "Default Environment"},
{"name": "Alert_Id", "originalName": "Alert_Id", "value": "Testing Alert_00000000-0000-0000-0000-000000000001"}
]
},
{
"displayName": "Entity",
"highlighted": false,
"hidden": false,
"items": [{"name": "Is Pivot", "originalName": "IsPivot", "value": "False"}]
},
{
"displayName": "Enrichment",
"highlighted": false,
"hidden": false,
"items": [{"name": "Total Score", "originalName": "Total Score", "value": "42/72"}]
}
],
"alertIdentifier": "Testing Alert_00000000-0000-0000-0000-000000000001",
"caseId": 306082,
"alertId": "1001",
"identifier": "192.0.2.1"
}
}
}
Human Readable Output
Added Entity Property with key
total_scoreand value42/72.
gcb-case-alert-entity-property-update
Update an existing custom property value on an involved entity in a case alert.
Base Command
gcb-case-alert-entity-property-update
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_id | Specify the ID of the case alert. Note: Use gcb-case-alert-list to retrieve alert ID. |
Required |
| entity_id | Specify the ID of the involved entity to update the property. Note: Use gcb-case-alert-entity-list to retrieve entity ID. |
Required |
| key | Specify the existing property key whose value should be updated. | Required |
| value | Specify the new value for the property. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.AlertEntity.id | String | The immutable identifier of the Involved Entity. |
| GoogleSecOps.AlertEntity.identifier | String | The identifier name of the Involved Entity. |
| GoogleSecOps.AlertEntity.type | String | The type of the Involved Entity. |
| GoogleSecOps.AlertEntity.alertId | String | The Alert ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.caseId | Number | The Case ID the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.alertIdentifier | String | The alert identifier the Involved Entity belongs to. |
| GoogleSecOps.AlertEntity.environment | String | The environment of the Involved Entity. |
| GoogleSecOps.AlertEntity.threatSource | String | The threat source associated with the Involved Entity. |
| GoogleSecOps.AlertEntity.operatingSystem | String | The operating system of the Involved Entity. |
| GoogleSecOps.AlertEntity.networkTitle | String | The network name related to the Involved Entity. |
| GoogleSecOps.AlertEntity.networkPriority | Number | The network priority of the Involved Entity. |
| GoogleSecOps.AlertEntity.entityUri | String | The full URL of the Involved Entity in the source system. |
| GoogleSecOps.AlertEntity.sourceSystemUri | String | The source system URI link of the Involved Entity. |
| GoogleSecOps.AlertEntity.additionalProperties | String | The additional properties of the Involved Entity as a JSON string. |
| GoogleSecOps.AlertEntity.suspicious | Boolean | Whether the Involved Entity is considered suspicious. |
| GoogleSecOps.AlertEntity.internal | Boolean | Whether the Involved Entity is internal. |
| GoogleSecOps.AlertEntity.attacker | Boolean | Whether the Involved Entity represents an attacker. |
| GoogleSecOps.AlertEntity.pivot | Boolean | Whether the Involved Entity is a pivot entity common to multiple cases. |
| GoogleSecOps.AlertEntity.manuallyCreated | Boolean | Whether the Involved Entity was added manually. |
| GoogleSecOps.AlertEntity.enriched | Boolean | Whether the Involved Entity has been enriched by an external action. |
| GoogleSecOps.AlertEntity.artifact | Boolean | Whether the Involved Entity is an artifact. |
| GoogleSecOps.AlertEntity.vulnerable | Boolean | Whether the Involved Entity is vulnerable. |
| GoogleSecOps.AlertEntity.fields.displayName | String | The display name of the context group of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.highlighted | Boolean | Whether the context group of the Involved Entity is highlighted. |
| GoogleSecOps.AlertEntity.fields.hidden | Boolean | Whether the context group of the Involved Entity is hidden. |
| GoogleSecOps.AlertEntity.fields.items.name | String | The property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.originalName | String | The original property name of the context group item of the Involved Entity. |
| GoogleSecOps.AlertEntity.fields.items.value | String | The value of the context group item of the Involved Entity. |
Command example
!gcb-case-alert-entity-property-update case_id=306082 alert_id=1001 entity_id=376398 key=total_score value=68/72
Context Example
{
"GoogleSecOps": {
"AlertEntity": {
"id": "376398",
"type": "ADDRESS",
"suspicious": true,
"internal": false,
"networkPriority": 5,
"attacker": true,
"pivot": false,
"environment": "Default Environment",
"manuallyCreated": true,
"additionalProperties": "{\"Type\":\"ADDRESS\",\"IsArtifact\":\"False\",\"IsEnriched\":\"False\",\"IsVulnerable\":\"False\",\"IsInternalAsset\":\"False\",\"OriginalIdentifier\":\"192.0.2.1\"}",
"enriched": true,
"artifact": false,
"vulnerable": false,
"fields": [
{
"displayName": "Default",
"highlighted": false,
"hidden": false,
"items": [
{"name": "Network_Priority", "originalName": "Network_Priority", "value": "5"},
{"name": "IsSuspicious", "originalName": "IsSuspicious", "value": "True"},
{"name": "IsAttacker", "originalName": "IsAttacker", "value": "True"},
{"name": "IsManuallyCreated", "originalName": "IsManuallyCreated", "value": "True"},
{"name": "Environment", "originalName": "Environment", "value": "Default Environment"},
{"name": "Alert_Id", "originalName": "Alert_Id", "value": "Testing Alert_00000000-0000-0000-0000-000000000001"}
]
},
{
"displayName": "Entity",
"highlighted": false,
"hidden": false,
"items": [{"name": "Is Pivot", "originalName": "IsPivot", "value": "False"}]
},
{
"displayName": "Enrichment",
"highlighted": false,
"hidden": false,
"items": [{"name": "Total Score", "originalName": "Total Score", "value": "68/72"}]
}
],
"alertIdentifier": "Testing Alert_00000000-0000-0000-0000-000000000001",
"caseId": 306082,
"alertId": "1001",
"identifier": "192.0.2.1"
}
}
}
Human Readable Output
Updated Entity Property with key
total_scoreand value68/72.
gcb-playbook-list
Retrieve the list of all playbooks that are currently enabled and ready for execution.
Base Command
gcb-playbook-list
Input
| Argument Name | Description | Required |
|---|---|---|
| environment | Filter the playbooks by environment. | Optional |
| execution_scope | Filter the playbooks attachable by the specified execution scope. Possible values are: ALERT, CASE, EXECUTION_SCOPE_UNSPECIFIED. Default is ALERT. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.Playbook.playbookName | String | The name of the enabled playbook. |
| GoogleSecOps.Playbook.description | String | The description of the playbook. |
| GoogleSecOps.Playbook.playbookType | String | The type of the playbook (REGULAR or NESTED). |
| GoogleSecOps.Playbook.originalWorkflowDefinitionIdentifier | String | The original workflow definition identifier of the playbook. |
| GoogleSecOps.Playbook.workflowDefinitionIdentifier | String | The workflow definition identifier of the playbook. |
| GoogleSecOps.Playbook.isDebugMode | Boolean | Whether the playbook is running in debug mode. |
Command example
!gcb-playbook-list execution_scope=ALERT
Context Example
{
"GoogleSecOps": {
"Playbook": [
{
"playbookName": "IMPORT 5 - Network Containment Block - Google",
"description": "Automated IP containment playbook that queries Google using CVE identifiers, extracts malicious IPs, creates entities in alerts, and blocks them via Zscaler. Supports real-time threat intelligence-based network containment.",
"playbookType": "NESTED",
"isDebugMode": true,
"originalWorkflowDefinitionIdentifier": "00000000-0000-0000-0000-000000000001",
"workflowDefinitionIdentifier": "00000000-0000-0000-0000-000000000001"
},
{
"playbookName": "Google Cloud Compute Platform Starting Playbook",
"description": "Google Cloud Compute Platform Starting Playbook provides reference implementation of how Google Cloud Compute Platform alerts can be processed in Google SecOps.",
"playbookType": "REGULAR",
"isDebugMode": false,
"originalWorkflowDefinitionIdentifier": "00000000-0000-0000-0000-000000000002",
"workflowDefinitionIdentifier": "00000000-0000-0000-0000-000000000002"
},
{
"playbookName": "Set Initial Severity",
"description": "An embedded workflow that can receive inputs and return an output.",
"playbookType": "NESTED",
"isDebugMode": true,
"originalWorkflowDefinitionIdentifier": "00000000-0000-0000-0000-000000000003",
"workflowDefinitionIdentifier": "00000000-0000-0000-0000-000000000003"
},
{
"playbookName": "Google - Network Containment Block",
"description": "Automated IP containment playbook that queries Google using CVE identifiers, extracts malicious IPs, creates entities in alerts, and blocks them via Zscaler. Supports real-time threat intelligence-based network containment.",
"playbookType": "NESTED",
"isDebugMode": false,
"originalWorkflowDefinitionIdentifier": "00000000-0000-0000-0000-000000000004",
"workflowDefinitionIdentifier": "00000000-0000-0000-0000-000000000005"
}
]
}
}
Human Readable Output
Enabled Playbooks
Playbook Name Description Playbook Type Original Workflow Definition Identifier Workflow Definition Identifier Is Debug Mode IMPORT 5 - Network Containment Block - Google Automated IP containment playbook that queries Google using CVE identifiers, extracts malicious IPs, creates entities in alerts, and blocks them via Zscaler. Supports real-time threat intelligence-based network containment. NESTED 00000000-0000-0000-0000-000000000001 00000000-0000-0000-0000-000000000001 True Google Cloud Compute Platform Starting Playbook Google Cloud Compute Platform Starting Playbook provides reference implementation of how Google Cloud Compute Platform alerts can be processed in Google SecOps. REGULAR 00000000-0000-0000-0000-000000000002 00000000-0000-0000-0000-000000000002 False Set Initial Severity An embedded workflow that can receive inputs and return an output. NESTED 00000000-0000-0000-0000-000000000003 00000000-0000-0000-0000-000000000003 True Google - Network Containment Block Automated IP containment playbook that queries Google using CVE identifiers, extracts malicious IPs, creates entities in alerts, and blocks them via Zscaler. Supports real-time threat intelligence-based network containment. NESTED 00000000-0000-0000-0000-000000000004 00000000-0000-0000-0000-000000000005 False
gcb-playbook-attach
Manually attach (trigger) a specific playbook to a case alert.
Base Command
gcb-playbook-attach
Input
| Argument Name | Description | Required |
|---|---|---|
| case_id | Specify the ID of the case to attach the playbook. Note: Use gcb-case-list to retrieve case ID. |
Required |
| alert_group_identifier | Specify the alert group identifier of the case alert. Note: Use gcb-case-alert-get to retrieve the alert group identifier. |
Required |
| alert_identifier | Specify the alert identifier of the case alert. Note: Use gcb-case-alert-get to retrieve the alert identifier. |
Required |
| playbook_name | Specify the name of the playbook (workflow) to attach. Note: Use gcb-playbook-list to retrieve available playbook names. |
Required |
| original_workflow_definition_identifier | Specify the original workflow definition identifier of the playbook. Note: Use gcb-playbook-list to retrieve the original workflow definition identifier. |
Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleSecOps.PlaybookAttach.caseId | String | The Case ID the playbook was attached to. |
| GoogleSecOps.PlaybookAttach.alertGroupIdentifier | String | The alert group identifier the playbook was attached to. |
| GoogleSecOps.PlaybookAttach.alertIdentifier | String | The alert identifier the playbook was attached to. |
| GoogleSecOps.PlaybookAttach.playbookName | String | The name of the playbook that was attached. |
| GoogleSecOps.PlaybookAttach.originalWorkflowDefinitionIdentifier | String | The original workflow definition identifier of the attached playbook. |
| GoogleSecOps.PlaybookAttach.success | Boolean | Whether the playbook was successfully attached to the alert. |
Command example
!gcb-playbook-attach case_id=1001 alert_group_identifier="Access Disabled Accounts_00000000-0000-0000-0000-000000000001" alert_identifier="ACCESS DISABLED ACCOUNTS_00000000-0000-0000-0000-000000000002" playbook_name="Phishing Investigation" original_workflow_definition_identifier="00000000-0000-0000-0000-000000000001"
Context Example
{
"GoogleSecOps": {
"PlaybookAttach": {
"caseId": "1001",
"alertGroupIdentifier": "Access Disabled Accounts_00000000-0000-0000-0000-000000000001",
"alertIdentifier": "ACCESS DISABLED ACCOUNTS_00000000-0000-0000-0000-000000000002",
"playbookName": "Phishing Investigation",
"originalWorkflowDefinitionIdentifier": "00000000-0000-0000-0000-000000000001",
"success": true
}
}
}
Human Readable Output
Playbook ‘Phishing Investigation’ successfully attached to alert ACCESS DISABLED ACCOUNTS_00000000-0000-0000-0000-000000000002 in case 1001.
Configuration parameters
credentials— (required)url_format— API URL Formatsecops_project_instance_id— Google SecOps Project Instance ID (required)secops_project_number— Google SecOps Project Numberregion— Region (required)other_region— Other RegionisFetch— Fetch incidentsincidentType— Incident typefirst_fetch— First Fetch Timemax_fetch— How many incidents to fetch each timecase_priorities— Case Prioritiescase_statuses— Case Statusescase_environments— Case Environmentscase_tags— Case Tagscase_filter_logic— Case Filter LogicincidentFetchInterval— Incidents Fetch Intervalinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (36)
-
gcb-case-alert-customfield-listRetrieve the list of custom field values associated with a case alert.
-
gcb-case-alert-entity-createManually create a new involved entity within a case alert.
-
gcb-case-alert-entity-getRetrieve detailed information about a specific involved entity in a case alert.
-
gcb-case-alert-entity-listRetrieve the list of entities associated with a case alert.
-
gcb-case-alert-entity-property-addAdd a new custom property to an involved entity in a case alert.
-
gcb-case-alert-entity-property-updateUpdate an existing custom property value on an involved entity in a case alert.
-
gcb-case-alert-entity-updateUpdate the attributes of an existing involved entity in a case alert.
-
gcb-case-alert-getRetrieve detailed information about a specific case alert by its ID.
-
gcb-case-alert-listRetrieve the list of alerts associated with the specified case.
-
gcb-case-alert-moveMove a case alert to a different case. Note: Both source and destination cases must be open.
-
gcb-case-alert-recommendation-createInitiate an asynchronous AI recommendation for a case alert.
-
gcb-case-alert-recommendation-fetchFetch a previously generated AI recommendation for a case alert.
-
gcb-case-alert-sla-pausePause the SLA timer for the specified case alert.
-
gcb-case-alert-sla-resumeResume the SLA timer for the specified case alert.
-
gcb-case-alert-sla-setSet the SLA parameters for a case alert. Note: When critical_time is specified, total_time must be greater than critical_time.
-
gcb-case-alert-tag-addAdd a tag to a case alert.
-
gcb-case-alert-tag-removeRemove a tag from a case alert.
-
gcb-case-alert-updateUpdate the properties of an existing case alert.
-
gcb-case-assignAssign the specified cases to a specific analyst or SOC role.
-
gcb-case-closeClose the specified cases.
-
gcb-case-close-definition-listRetrieve the list of case close definitions configured in the instance.
-
gcb-case-comment-createAdd a comment to the specified case.
-
gcb-case-comment-listRetrieve the list of comments associated with the specified case.
-
gcb-case-getRetrieve a specific case by its ID.
-
gcb-case-listRetrieve the list of cases.
-
gcb-case-priority-changeChange the priority of the specified cases.
-
gcb-case-reopenReopen the specified cases.
-
gcb-case-sla-pausePause the SLA timer for the specified case.
-
gcb-case-sla-resumeResume the SLA timer for the specified case.
-
gcb-case-stage-changeChange the workflow stage of the specified cases.
-
gcb-case-stage-definition-listRetrieve the list of case stage definitions configured in the instance.
-
gcb-case-tag-addAdd the specified tags to the cases.
-
gcb-case-tag-removeRemove the specified tag from a case.
-
gcb-case-updateUpdate the properties of a case.
-
gcb-playbook-attachManually attach (trigger) a specific playbook to a case alert.
-
gcb-playbook-listRetrieve the list of all playbooks that are currently enabled and ready for execution.
"""Main file for GoogleSecOpsCases Integration.""" import calendar import hashlib from copy import deepcopy from datetime import UTC, datetime, timedelta from typing import Any import demistomock as demisto from CommonServerPython import * from google.auth.transport import requests as auth_requests from google.oauth2 import service_account """ CONSTANTS """ DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" HR_DATE_FORMAT = "%Y-%m-%d %H:%M:%S UTC" SCOPES = ["https://www.googleapis.com/auth/cloud-platform"] STATUS_LIST_TO_RETRY = (429, *(status_code for status_code in requests.status_codes._codes if status_code >= 500)) # type: ignore OK_CODES = (200, 201, 204) MAX_RETRIES = 4 BACKOFF_FACTOR = 7.5 DEFAULT_PAGE_SIZE = 50 DEFAULT_CASE_LIST_SORT_ORDER = "Desc" DEFAULT_CASE_LIST_SORT_BY = "createTime" DEFAULT_COMMENT_SORT_ORDER = "Desc" DEFAULT_COMMENT_SORT_BY = "createTime" DEFAULT_ALERT_ENTITY_SORT_BY = "id" DEFAULT_ALERT_ENTITY_SORT_ORDER = "Desc" MAX_PAGE_SIZE = 1000 SECOPS_V1_ALPHA_URL = "https://chronicle.{}.rep.googleapis.com/v1alpha" OLDER_SECOPS_V1_ALPHA_URL = "https://{}-chronicle.googleapis.com/v1alpha" DEFAULT_REGION = "us" DEFAULT_FIRST_FETCH = "3 days" MAX_FIRST_FETCH_DAYS = 7 MAX_FIRST_FETCH_BOUNDARY_STRINGS = ["7 day", "168 hour", "1 week"] DEFAULT_MAX_FETCH = 50 MAX_FETCH_LIMIT = 200 CASES_EXPAND_FIELDS = "products,tasks,tags,closureDetails,sla,alertsSla" CASE_ALERT_EXPAND_FIELDS = "sla,involvedRelations,tags,closureDetails" CASE_SEVERITY_MAP = { "PRIORITY_CRITICAL": 4, "PRIORITY_HIGH": 3, "PRIORITY_MEDIUM": 2, "PRIORITY_LOW": 1, "PRIORITY_INFO": 0.5, "PRIORITY_UNSPECIFIED": 0, } DEFAULT_FILTER_LOGIC = "AND" VALID_CASE_FILTER_LOGIC = (DEFAULT_FILTER_LOGIC, "OR") VALID_CASE_PRIORITIES = ("UNSPECIFIED", "INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL") VALID_CASE_STATUSES = ("CASE_DATA_STATE_UNSPECIFIED", "OPENED", "CLOSED", "MERGED", "CREATION_PENDING") VALID_CASE_WORKFLOW_STATUSES = ( "WORKFLOW_STATUS_UNSPECIFIED", "NONE", "IN_PROGRESS", "COMPLETED", "FAILED", "TERMINATED", "PENDING_IN_QUEUE", "PENDING_FOR_USER", ) VALID_CASE_SLA_STATUSES = ("SLA_EXPIRATION_STATUS_UNSPECIFIED", "OPEN_SLA", "PASSED_DUE", "NO_SLA", "CRITICAL_EXPIRED", "PAUSED") VALID_CASE_CLOSE_REASONS = ("MALICIOUS", "NOT_MALICIOUS", "MAINTENANCE", "INCONCLUSIVE", "UNKNOWN", "CLOSE_REASON_UNSPECIFIED") VALID_CASE_TYPES = ("EXTERNAL", "TEST", "REQUEST", "CASE_TYPE_UNSPECIFIED") VALID_SORT_ORDERS = ("Asc", "Desc") CASE_UPDATE_ARGS = ("display_name", "description", "important", "incident") VALID_CASE_ALERT_PRIORITIES = ( "LEGACY_CASE_PRIORITY_UNSPECIFIED", "UNCHANGED", "INFORMATIVE", "LOW", "MEDIUM", "HIGH", "CRITICAL", ) VALID_CASE_ALERT_STATUSES = ("OPEN", "CLOSE", "ALERT_STATUS_UNSPECIFIED") DEFAULT_CASE_ALERT_LIST_SORT_BY = "createTime" DEFAULT_CASE_ALERT_LIST_SORT_ORDER = "Desc" CASE_ALERT_UPDATE_ARGS = ("status", "priority") CASE_ALERT_ENTITY_UPDATE_ARGS = ( "suspicious", "internal", "attacker", "pivot", "operating_system", "network_title", "threat_source", "network_priority", ) VALID_CASE_ALERT_CLOSE_REASONS = ( "MALICIOUS", "NOT_MALICIOUS", "MAINTENANCE", "INCONCLUSIVE", "UNKNOWN", "CLOSE_REASON_UNSPECIFIED", ) DEFAULT_EXECUTION_SCOPE = "ALERT" VALID_EXECUTION_SCOPES = (DEFAULT_EXECUTION_SCOPE, "CASE", "EXECUTION_SCOPE_UNSPECIFIED") CASE_ID_DISPLAY = "Case ID" ALERT_ID_DISPLAY = "Alert ID" ENTITY_ID_DISPLAY = "Entity ID" ENDPOINTS = { "CASES": "/cases", "CASE": "/cases/{case_id}", "CASES_BULK_ADD_TAG": "/cases:executeBulkAddTag", "CASES_REMOVE_TAG": "/cases/{case_id}:removeTag", "CASES_BULK_CHANGE_PRIORITY": "/cases:executeBulkChangePriority", "CASE_STAGE_DEFINITIONS": "/caseStageDefinitions", "CASES_BULK_CHANGE_STAGE": "/cases:executeBulkChangeStage", "CASES_BULK_REOPEN": "/cases:executeBulkReopen", "CASE_CLOSE_DEFINITIONS": "/caseCloseDefinitions", "CASES_BULK_CLOSE": "/cases:executeBulkClose", "CASES_BULK_ASSIGN": "/cases:executeBulkAssign", "LEGACY_SOAR_USERS": "/legacySoarUsers", "CASE_COMMENTS": "/cases/{case_id}/caseComments", "CASE_SLA_PAUSE": "/cases/{case_id}:pauseSla", "CASE_SLA_RESUME": "/cases/{case_id}:resumeSla", "CASE_ALERTS": "/cases/{case_id}/caseAlerts", "CASE_ALERT": "/cases/{case_id}/caseAlerts/{alert_id}", "CASE_ALERT_ADD_TAG": "/cases/{case_id}/caseAlerts/{alert_id}:addTag", "CASE_ALERT_REMOVE_TAG": "/cases/{case_id}/caseAlerts/{alert_id}:removeTag", "CASE_ALERT_MOVE": "/cases/{case_id}/caseAlerts/{alert_id}:move", "CASE_ALERT_SLA_PAUSE": "/cases/{case_id}/caseAlerts/{alert_id}:pauseSla", "CASE_ALERT_SLA_RESUME": "/cases/{case_id}/caseAlerts/{alert_id}:resumeSla", "CASE_ALERT_SLA_SET": "/cases/{case_id}/caseAlerts/{alert_id}:setSla", "CASE_ALERT_RECOMMENDATION_CREATE": "/cases/{case_id}/caseAlerts/{alert_id}:createRecommendationLongRunning", "CASE_ALERT_FETCH_RECOMMENDATION": "/cases/{case_id}/caseAlerts:fetchRecommendation", "CASE_ALERT_CUSTOMFIELD_VALUES": "/cases/{case_id}/caseAlerts/{alert_id}/customFieldValues", "CUSTOM_FIELDS": "/customFields", "CASE_ALERT_INVOLVED_ENTITIES": "/cases/{case_id}/caseAlerts/{alert_id}/involvedEntities", "CASE_ALERT_INVOLVED_ENTITY": "/cases/{case_id}/caseAlerts/{alert_id}/involvedEntities/{entity_id}", "CASE_ALERT_INVOLVED_ENTITY_ADD_PROPERTY": "/cases/{case_id}/caseAlerts/{alert_id}/involvedEntities/{entity_id}:addProperty", "CASE_ALERT_INVOLVED_ENTITY_UPDATE_PROPERTY": ( "/cases/{case_id}/caseAlerts/{alert_id}/involvedEntities/{entity_id}:updateProperty" ), "LEGACY_PLAYBOOKS": "/legacyPlaybooks:legacyGetEnabledWFCards", "LEGACY_PLAYBOOK_ATTACH": "/legacyPlaybooks:legacyAttachNestedWorkflowToCase", } SECOPS_OUTPUT_PATHS = { "Case": "GoogleSecOps.Case", "CaseStageDefinition": "GoogleSecOps.CaseStageDefinition", "CaseCloseDefinition": "GoogleSecOps.CaseCloseDefinition", "CaseComment": "GoogleSecOps.CaseComment", "PageToken": "GoogleSecOps.PageToken", "CaseAlert": "GoogleSecOps.CaseAlert", "AlertRecommendation": "GoogleSecOps.AlertRecommendation", "AlertCustomFieldValue": "GoogleSecOps.AlertCustomFieldValue", "AlertEntity": "GoogleSecOps.AlertEntity", "Playbook": "GoogleSecOps.Playbook", "PlaybookAttach": "GoogleSecOps.PlaybookAttach", } MESSAGES = { "INTERNAL_SERVER_ERROR": ( "Status code: {}\nError: Internal server error occurred. Failed to execute request with {} retries.\nMessage: {}" ), "RATE_LIMIT_EXCEEDED": ( "Status code: {}\nError: API rate limit exceeded. Failed to execute request with {} retries.\nMessage: {}" ), "HTTP_ERROR": "Status code: {}\nError: {}", "EMPTY_RESPONSE": "Technical Error while making API call to Google SecOps. Empty response received with the status code: {}", "INVALID_JSON_RESPONSE": "Invalid response format while making API call to Google SecOps. Response not in JSON format", "TIME_IN_PAST": "Invalid value for '{arg_name}': '{time_str}' is in the past. Specify a future date or a positive duration.", "INVALID_PROJECT_NUMBER": "Google SecOps Project Number should be a positive number.", "MISSING_PROJECT_INSTANCE_ID": "Please Provide the Google SecOps Project Instance ID.", "MISSING_REGION": "Please Provide the valid region.", "INVALID_SERVICE_ACCOUNT_JSON": "User's Service Account JSON has invalid format", "REQUIRED_ARGUMENT": "Missing argument {}.", "INVALID_POSITIVE_INTEGER": "Invalid {}: {}. {} must be a positive integer.", "VALIDATE_SINGLE_SELECT": "{} can have one of these values only {}.", "INVALID_MAX_FETCH": "Incidents fetch limit should be in the range from 1 to {max_limit}. Got: {value}.", "INVALID_FIRST_FETCH": ( "Invalid value for 'First Fetch Time': {}. " "The value cannot be older than 7 days or 168 hours (in relative manner compared to current time)." ), "FUTURE_DATE": "First fetch time should not be in the future.", "PERMISSION_DENIED": "Permission denied", "NO_RECORDS_FOUND": "No {} found.", "USER_NOT_FOUND": "No active user found with the provided email: {}.", "INVALID_INT_RANGE": "Invalid value '{}' for argument '{}'. Expected a value between {} and {}.", "INVALID_DATE_RANGE": "{} must be less than or equal to {}.", "AT_LEAST_ONE_REQUIRED": "At least one of the following arguments must be provided: {}.", "INVALID_NON_NEGATIVE_INTEGER": "Invalid value '{}' for argument '{}'. Value must be a non-negative integer.", } """ CLIENT CLASS """ class Client: """ Client to use in integration to fetch data from Google SecOps. requires service_account_credentials : a json formatted string act as a token access """ def __init__(self, params: dict[str, Any], proxy, disable_ssl): """ Initialize HTTP Client. :param params: parameter returned from demisto.params() :param proxy: whether to use environment proxy :param disable_ssl: whether to disable ssl """ encoded_service_account = str(params.get("credentials", {}).get("password", "")) service_account_credential = json.loads(encoded_service_account, strict=False) self.project_id = service_account_credential.get("project_id", "") # Create a credential using the Google Developer Service Account Credential and Google SecOps API scope. credentials = service_account.Credentials.from_service_account_info(service_account_credential, scopes=SCOPES) proxies = {} if proxy: proxies = handle_proxy() if not proxies.get("https", True): raise DemistoException("https proxy value is empty. Check Demisto server configuration" + str(proxies)) https_proxy = proxies["https"] if not https_proxy.startswith("https") and not https_proxy.startswith("http"): proxies["https"] = "https://" + https_proxy else: skip_proxy() # Build an HTTP client which can make authorized OAuth requests. self.http_client = auth_requests.AuthorizedSession(credentials) self.proxy_info = proxies self.disable_ssl = disable_ssl self._implement_retry(retries=MAX_RETRIES, status_list_to_retry=STATUS_LIST_TO_RETRY, backoff_factor=BACKOFF_FACTOR) region = params.get("region", DEFAULT_REGION).lower() other_region = params.get("other_region", "").strip().lower() self.project_location = region if region != "other" else other_region self.project_instance_id = params.get("secops_project_instance_id", "") secops_project_number = params.get("secops_project_number", "") self.project_number = secops_project_number if secops_project_number else self.project_id url_format = params.get("url_format", "<chronicle>.<REGION>.<rep.googleapis.com>").lower() self.use_new_url_format = url_format == "<chronicle>.<region>.<rep.googleapis.com>" def _implement_retry( self, retries=0, status_list_to_retry=None, backoff_factor=5, raise_on_redirect=False, raise_on_status=False ): """ Implements the retry mechanism. In the default case where retries = 0 the request will fail on the first time. :type retries: ``int`` :param retries: How many retries should be made in case of a failure. when set to '0'- will fail on the first time. :type status_list_to_retry: ``iterable`` :param status_list_to_retry: A set of integer HTTP status codes that we should force a retry on. A retry is initiated if the request method is in ['GET', 'POST', 'PUT'] and the response status code is in ``status_list_to_retry``. :type backoff_factor: ``float`` :param backoff_factor: A backoff factor to apply between attempts after the second try (most errors are resolved immediately by a second try without a delay). urllib3 will sleep for:: {backoff factor} * (2 ** ({number of total retries} - 1)) seconds. If the backoff_factor is 0.1, then :func:`.sleep` will sleep for [0.0s, 0.2s, 0.4s, ...] between retries. It will never be longer than :attr:`Retry.BACKOFF_MAX`. By default, backoff_factor set to 5 :type raise_on_redirect: ``bool`` :param raise_on_redirect: Whether, if the number of redirects is exhausted, to raise a MaxRetryError, or to return a response with a response code in the 3xx range. :type raise_on_status: ``bool`` :param raise_on_status: Similar meaning to ``raise_on_redirect``: whether we should raise an exception, or return a response, if status falls in ``status_forcelist`` range and retries have been exhausted. """ try: method_whitelist = ( "allowed_methods" if hasattr( Retry.DEFAULT, # type: ignore[attr-defined] "allowed_methods", ) else "method_whitelist" ) whitelist_kawargs = {method_whitelist: frozenset(["GET", "POST", "PUT"])} retry = Retry( total=retries, read=retries, connect=retries, backoff_factor=backoff_factor, status=retries, status_forcelist=status_list_to_retry, raise_on_status=raise_on_status, raise_on_redirect=raise_on_redirect, **whitelist_kawargs, # type: ignore[arg-type] ) http_adapter = HTTPAdapter(max_retries=retry) if not self.disable_ssl: https_adapter = http_adapter elif IS_PY3 and PY_VER_MINOR >= 10: https_adapter = SSLAdapter(max_retries=retry, verify=not self.disable_ssl) # type: ignore[arg-type] else: https_adapter = http_adapter self.http_client.mount("https://", https_adapter) except NameError as e: demisto.debug(f"_implement_retry: NameError {e}") @staticmethod def _parse_error_message(error: str, region: str) -> str: """ Extract error message from error object. :type error: str :param error: Error string response to be parsed. :type region: str :param region: Region value based on the location of the Google SecOps instance. :return: error message :rtype: str """ try: json_error = json.loads(error) if isinstance(json_error, list): json_error = json_error[0] except json.decoder.JSONDecodeError: if region.lower() == "other" and "404" in error: error_message = 'Invalid response from Google SecOps API. Check the provided "Other Region" parameter.' else: error_message = "Invalid response received from SecOps API. Response not in JSON format." demisto.debug(f"{error_message} Response - {error}") return error_message error_obj = json_error.get("error", {}) if error_obj.get("code") == 403: return MESSAGES["PERMISSION_DENIED"] message = error_obj.get("message", "") status = error_obj.get("status") if status: message += f"\nStatus: {status}" return message def _create_url_path(self) -> str: """ Creates the base URL path for the Google SecOps API. :return: Constructed URL path including project, location and instance. :rtype: str """ parent = f"projects/{self.project_number}/locations/{self.project_location}/instances/{self.project_instance_id}" if self.use_new_url_format: url = SECOPS_V1_ALPHA_URL.format(self.project_location) else: url = OLDER_SECOPS_V1_ALPHA_URL.format(self.project_location) return f"{url}/{parent}" def validate_response( self, url_suffix: str, method: str = "GET", body=None, json_data: dict | None = None, params=None, allow_empty_response: bool = False, ) -> dict: """ Get response from Google SecOps API and validate it. :type url_suffix: str :param url_suffix: URL suffix appended to the base path. :type method: str :param method: HTTP request method. :type body: str :param body: Data to pass with the request. :type json_data: dict :param json_data: JSON-serializable dict to send as request body (sets Content-Type: application/json automatically). :type params: dict :param params: Query parameters to pass with the request. :type allow_empty_response: bool :param allow_empty_response: Whether to allow an empty response body. :return: Parsed JSON response. :rtype: dict """ url = urljoin(self._create_url_path(), url_suffix) demisto.debug(f"[SECOPS]: Request URL: {url}, params = {params}, data = {body}, json = {json_data}") raw_response = self.http_client.request( url=url, method=method, data=body, json=json_data, params=params, proxies=self.proxy_info, verify=not self.disable_ssl ) if 500 <= raw_response.status_code <= 599: raise ValueError( MESSAGES["INTERNAL_SERVER_ERROR"].format( raw_response.status_code, MAX_RETRIES, self._parse_error_message(raw_response.text, self.project_location) ) ) if raw_response.status_code == 429: raise ValueError( MESSAGES["RATE_LIMIT_EXCEEDED"].format( raw_response.status_code, MAX_RETRIES, self._parse_error_message(raw_response.text, self.project_location) ) ) if raw_response.status_code in (400, 404): raise ValueError( MESSAGES["HTTP_ERROR"].format( raw_response.status_code, self._parse_error_message(raw_response.text, self.project_location) ) ) if raw_response.status_code not in OK_CODES: raise ValueError( MESSAGES["HTTP_ERROR"].format( raw_response.status_code, self._parse_error_message(raw_response.text, self.project_location) ) ) if not raw_response.text: if allow_empty_response: return {} raise ValueError(MESSAGES["EMPTY_RESPONSE"].format(raw_response.status_code)) try: return raw_response.json() except json.decoder.JSONDecodeError: raise ValueError(MESSAGES["INVALID_JSON_RESPONSE"]) def list_cases( self, page_size: int, case_filter: str | None = None, page_token: str | None = None, order_by: str | None = None, ) -> dict[str, Any]: """ Call Google SecOps API to list Cases. :type page_size: int :param page_size: Maximum number of cases to return. :type case_filter: str :param case_filter: AIP-160 filter string. :type page_token: str :param page_token: Page token to resume pagination. :type order_by: str :param order_by: Field and direction to order results by. :return: Raw API response dict containing 'cases' list and optional 'nextPageToken'. :rtype: dict[str, Any] """ params: dict[str, Any] = { "pageSize": page_size, "expand": CASES_EXPAND_FIELDS, "filter": case_filter, "orderBy": order_by, "pageToken": page_token, } remove_nulls_from_dictionary(params) return self.validate_response(ENDPOINTS["CASES"], params=params) def get_case(self, case_id: str) -> dict[str, Any]: """ Retrieve a specific Case by its ID. :type case_id: str :param case_id: The ID of the case to retrieve. :return: Raw API response dict for the case. :rtype: dict[str, Any] """ url_suffix = ENDPOINTS["CASE"].format(case_id=case_id) params: dict[str, Any] = {"expand": CASES_EXPAND_FIELDS} return self.validate_response(url_suffix, params=params) def update_case(self, case_id: str, body: dict[str, Any], update_mask: str) -> dict[str, Any]: """ Update properties of a Case using PATCH. :type case_id: str :param case_id: The ID of the case to update. :type body: dict[str, Any] :param body: Fields to update. :type update_mask: str :param update_mask: Comma-separated field paths for the updateMask query parameter. :return: Raw API response dict for the updated case. :rtype: dict[str, Any] """ url_suffix = ENDPOINTS["CASE"].format(case_id=case_id) params: dict[str, Any] = {"updateMask": update_mask} return self.validate_response(url_suffix, method="PATCH", json_data=body, params=params) def case_tag_add(self, case_ids: list[str], tags: list[str]) -> None: """ Add tags to cases in bulk using the executeBulkAddTag API. :type case_ids: list[str] :param case_ids: List of case IDs to add tags to. :type tags: list[str] :param tags: List of tags to add. """ body = {"casesIds": case_ids, "tags": tags} self.validate_response(ENDPOINTS["CASES_BULK_ADD_TAG"], method="POST", json_data=body, allow_empty_response=True) def case_tag_remove(self, case_id: str, tag: str) -> None: """ Remove a tag from a case using the removeTag API. :type case_id: str :param case_id: The ID of the case to remove the tag from. :type tag: str :param tag: The tag to remove. """ url_suffix = ENDPOINTS["CASES_REMOVE_TAG"].format(case_id=case_id) body = {"tag": tag} self.validate_response(url_suffix, method="POST", json_data=body, allow_empty_response=True) def case_priority_change(self, case_ids: list[str], priority: str) -> None: """ Change the priority of cases in bulk using the executeBulkChangePriority API. :type case_ids: list[str] :param case_ids: List of case IDs to change priority for. :type priority: str :param priority: The new priority value (will be converted to PRIORITY_<VALUE> format). """ body = {"casesIds": case_ids, "priority": f"PRIORITY_{priority}"} self.validate_response(ENDPOINTS["CASES_BULK_CHANGE_PRIORITY"], method="POST", json_data=body, allow_empty_response=True) def case_stage_definitions_list(self) -> dict[str, Any]: """ Retrieve the list of case stage definitions configured in the instance. :return: Raw API response dict containing 'caseStageDefinitions' list. :rtype: dict[str, Any] """ params: dict[str, Any] = { "pageSize": MAX_PAGE_SIZE, "orderBy": "order", } return self.validate_response(ENDPOINTS["CASE_STAGE_DEFINITIONS"], params=params) def case_stage_change(self, case_ids: list[str], stage: str) -> None: """ Change the workflow stage of cases in bulk using the executeBulkChangeStage API. :type case_ids: list[str] :param case_ids: List of case IDs to change stage for. :type stage: str :param stage: The new workflow stage value. """ body = {"casesIds": case_ids, "stage": stage} self.validate_response(ENDPOINTS["CASES_BULK_CHANGE_STAGE"], method="POST", json_data=body, allow_empty_response=True) def case_reopen(self, case_ids: list[str], comment: str) -> None: """ Reopen cases in bulk using the executeBulkReopen API. :type case_ids: list[str] :param case_ids: List of case IDs to reopen. :type comment: str :param comment: Comment explaining why the cases are being reopened. """ body = {"casesIds": case_ids, "reopenComment": comment} self.validate_response(ENDPOINTS["CASES_BULK_REOPEN"], method="POST", json_data=body, allow_empty_response=True) def case_close_definitions_list(self) -> dict[str, Any]: """ Retrieve the list of case close definitions configured in the instance. :return: Raw API response dict containing 'caseCloseDefinitions' list. :rtype: dict[str, Any] """ params: dict[str, Any] = { "pageSize": MAX_PAGE_SIZE, "orderBy": "closeReason", } return self.validate_response(ENDPOINTS["CASE_CLOSE_DEFINITIONS"], params=params) def case_close(self, case_ids: list[str], close_reason: str, root_cause: str, close_comment: str | None = None) -> None: """ Close cases in bulk using the executeBulkClose API. :type case_ids: list[str] :param case_ids: List of case IDs to close. :type close_reason: str :param close_reason: The reason for closing the cases. :type root_cause: str :param root_cause: Root cause description for the closure. :type close_comment: str | None :param close_comment: Optional comment to add when closing the cases. """ body: dict[str, Any] = {"casesIds": case_ids, "closeReason": close_reason, "rootCause": root_cause} if close_comment: body["closeComment"] = close_comment self.validate_response(ENDPOINTS["CASES_BULK_CLOSE"], method="POST", json_data=body, allow_empty_response=True) def get_soar_user_ids(self, emails: list[str]) -> dict[str, str]: """ Resolve email addresses to SOAR user IDs via the legacySoarUsers API. :type emails: list[str] :param emails: List of user email addresses to resolve. :return: Mapping of lowercased email to user ID (displayName field) for active users only. :rtype: dict[str, str] """ email_filter = "(" + " OR ".join(f"email='{email}'" for email in emails) + ") AND accountState='ACTIVE'" params = {"pageSize": len(emails), "filter": email_filter} response = self.validate_response(ENDPOINTS["LEGACY_SOAR_USERS"], params=params) email_to_id: dict[str, str] = {} for user in response.get("legacySoarUsers", []): email = user.get("email", "").lower() user_id = user.get("displayName", "") if not email or not user_id: continue email_to_id[email] = user_id return email_to_id def case_assign(self, case_ids: list[str], assignee: str) -> None: """ Assign cases in bulk to an analyst or SOC role using the executeBulkAssign API. :type case_ids: list[str] :param case_ids: List of case IDs to assign. :type assignee: str :param assignee: The resolved user ID or SOC role (e.g. @Tier1). """ body = {"casesIds": case_ids, "userName": assignee} self.validate_response(ENDPOINTS["CASES_BULK_ASSIGN"], method="POST", json_data=body, allow_empty_response=True) def case_comments_list( self, case_id: str, page_size: int, page_token: str | None = None, sort_by: str | None = None, sort_order: str | None = None, ) -> dict[str, Any]: """ Retrieve the list of comments for a specified case. :type case_id: str :param case_id: The ID of the case to retrieve comments for. :type page_size: int :param page_size: Maximum number of comments to return. :type page_token: str | None :param page_token: Page token to resume pagination. :type sort_by: str | None :param sort_by: Field to sort results by. :type sort_order: str | None :param sort_order: Sort direction (Asc or Desc). :return: Raw API response dict containing 'caseComments' list and optional 'nextPageToken'. :rtype: dict[str, Any] """ params: dict[str, Any] = { "pageSize": page_size, "pageToken": page_token, "orderBy": f"{sort_by} {sort_order.lower()}" if sort_by and sort_order else None, } remove_nulls_from_dictionary(params) endpoint = ENDPOINTS["CASE_COMMENTS"].format(case_id=case_id) return self.validate_response(endpoint, params=params) def case_comment_create(self, case_id: str, comment: str) -> dict[str, Any]: """ Add a comment to the specified case. :type case_id: str :param case_id: The ID of the case to add the comment to. :type comment: str :param comment: The comment text to add. :return: Raw API response dict containing the created case comment details. :rtype: dict[str, Any] """ endpoint = ENDPOINTS["CASE_COMMENTS"].format(case_id=case_id) body = {"comment": comment} return self.validate_response(endpoint, method="POST", json_data=body) def case_sla_pause(self, case_id: str, message: str | None) -> None: """ Pause the SLA timer for the specified case. :type case_id: str :param case_id: The ID of the case to pause the SLA timer for. :type message: str | None :param message: The reason for pausing the SLA timer. Optional. """ endpoint = ENDPOINTS["CASE_SLA_PAUSE"].format(case_id=case_id) body = {"message": message} if message else {} self.validate_response(endpoint, method="POST", json_data=body, allow_empty_response=True) def case_sla_resume(self, case_id: str) -> None: """ Resume the SLA timer for the specified case. :type case_id: str :param case_id: The ID of the case to resume the SLA timer for. """ endpoint = ENDPOINTS["CASE_SLA_RESUME"].format(case_id=case_id) self.validate_response(endpoint, method="POST", allow_empty_response=True) def list_case_alerts( self, case_id: str, page_size: int, page_token: str | None, alert_filter: str | None, order_by: str | None, ) -> dict[str, Any]: """ Retrieve the list of Case Alerts associated with a specific Case. :type case_id: str :param case_id: The ID of the Case to list alerts for. :type page_size: int :param page_size: Maximum number of alerts to return. :type page_token: str | None :param page_token: Page token for pagination. :type alert_filter: str | None :param alert_filter: AIP-160 filter string. :type order_by: str | None :param order_by: Sort field and direction, e.g. "createTime Desc". :return: Raw API response dict containing 'caseAlerts' list and optional 'nextPageToken'. :rtype: dict[str, Any] """ url_suffix = ENDPOINTS["CASE_ALERTS"].format(case_id=case_id) params: dict[str, Any] = { "pageSize": page_size, "pageToken": page_token, "filter": alert_filter or None, "orderBy": order_by, "expand": CASE_ALERT_EXPAND_FIELDS, } remove_nulls_from_dictionary(params) return self.validate_response(url_suffix, params=params) def get_case_alert(self, case_id: str, alert_id: str) -> dict[str, Any]: """ Retrieve a specific Case Alert by its ID. :type case_id: str :param case_id: The ID of the case the alert belongs to. :type alert_id: str :param alert_id: The ID of the Case Alert to retrieve. :return: Raw API response dict for the Case Alert. :rtype: dict[str, Any] """ url_suffix = ENDPOINTS["CASE_ALERT"].format(case_id=case_id, alert_id=alert_id) params: dict[str, Any] = {"expand": CASE_ALERT_EXPAND_FIELDS} return self.validate_response(url_suffix, params=params) def update_case_alert(self, case_id: str, alert_id: str, body: dict[str, Any], update_mask: str) -> dict[str, Any]: """ Update properties of a Case Alert using PATCH. :type case_id: str :param case_id: The ID of the case the alert belongs to. :type alert_id: str :param alert_id: The ID of the alert to update. :type body: dict[str, Any] :param body: Fields to update. :type update_mask: str :param update_mask: Comma-separated field paths for the updateMask query parameter. :return: Raw API response dict for the updated Case Alert. :rtype: dict[str, Any] """ url_suffix = ENDPOINTS["CASE_ALERT"].format(case_id=case_id, alert_id=alert_id) params: dict[str, Any] = {"updateMask": update_mask} return self.validate_response(url_suffix, method="PATCH", json_data=body, params=params) def case_alert_tag_add(self, case_id: str, alert_id: str, tag: str) -> None: """ Add a tag to a Case Alert using the addTag API. :type case_id: str :param case_id: The ID of the Case the alert belongs to. :type alert_id: str :param alert_id: The ID of the Case Alert to add a tag to. :type tag: str :param tag: The tag to add to the alert. """ endpoint = ENDPOINTS["CASE_ALERT_ADD_TAG"].format(case_id=case_id, alert_id=alert_id) body = {"tag": tag} self.validate_response(endpoint, method="POST", json_data=body, allow_empty_response=True) def case_alert_tag_remove(self, case_id: str, alert_id: str, tag: str) -> None: """ Remove a tag from a Case Alert using the removeTag API. :type case_id: str :param case_id: The ID of the Case the alert belongs to. :type alert_id: str :param alert_id: The ID of the Case Alert to remove a tag from. :type tag: str :param tag: The tag to remove from the alert. """ endpoint = ENDPOINTS["CASE_ALERT_REMOVE_TAG"].format(case_id=case_id, alert_id=alert_id) body = {"tag": tag} self.validate_response(endpoint, method="POST", json_data=body, allow_empty_response=True) def case_alert_move(self, case_id: str, alert_id: str, destination_case_id: str) -> dict: """ Move a Case Alert to a different Case using the move API. :type case_id: str :param case_id: The ID of the source Case the alert belongs to. :type alert_id: str :param alert_id: The ID of the Case Alert to move. :type destination_case_id: str :param destination_case_id: The ID of the destination Case to move the alert to. :return: API response containing newCaseId and valid fields. :rtype: dict """ endpoint = ENDPOINTS["CASE_ALERT_MOVE"].format(case_id=case_id, alert_id=alert_id) body = {"destinationCaseId": int(destination_case_id)} return self.validate_response(endpoint, method="POST", json_data=body) def case_alert_sla_pause(self, case_id: str, alert_id: str, message: str | None) -> None: """ Pause the SLA timer for the specified Case Alert. :type case_id: str :param case_id: The ID of the Case the alert belongs to. :type alert_id: str :param alert_id: The ID of the Case Alert to pause the SLA timer for. :type message: str | None :param message: The reason for pausing the SLA timer. Optional. """ endpoint = ENDPOINTS["CASE_ALERT_SLA_PAUSE"].format(case_id=case_id, alert_id=alert_id) body = {"message": message} if message else {} self.validate_response(endpoint, method="POST", json_data=body, allow_empty_response=True) def case_alert_sla_resume(self, case_id: str, alert_id: str) -> None: """ Resume the SLA timer for the specified Case Alert. :type case_id: str :param case_id: The ID of the Case the alert belongs to. :type alert_id: str :param alert_id: The ID of the Case Alert to resume the SLA timer for. """ endpoint = ENDPOINTS["CASE_ALERT_SLA_RESUME"].format(case_id=case_id, alert_id=alert_id) self.validate_response(endpoint, method="POST", allow_empty_response=True) def case_alert_sla_set(self, case_id: str, alert_id: str, total_time_ms: int, critical_time_ms: int | None) -> None: """ Set the SLA parameters for the specified Case Alert. :type case_id: str :param case_id: The ID of the Case the alert belongs to. :type alert_id: str :param alert_id: The ID of the Case Alert to set the SLA for. :type total_time_ms: int :param total_time_ms: The total SLA duration in milliseconds. :type critical_time_ms: int | None :param critical_time_ms: The critical SLA threshold in milliseconds. Optional. """ endpoint = ENDPOINTS["CASE_ALERT_SLA_SET"].format(case_id=case_id, alert_id=alert_id) body: dict = {"totalTimeMs": str(total_time_ms)} if critical_time_ms is not None: body["criticalTimeMs"] = str(critical_time_ms) self.validate_response(endpoint, method="POST", json_data=body, allow_empty_response=True) def case_alert_recommendation_create(self, case_id: str, alert_id: str) -> dict[str, Any]: """ Initiate an asynchronous AI recommendation for a Case Alert. :type case_id: str :param case_id: The ID of the Case the alert belongs to. :type alert_id: str :param alert_id: The ID of the Case Alert to generate a recommendation for. :return: Response containing the recommendation ID. :rtype: dict[str, Any] """ endpoint = ENDPOINTS["CASE_ALERT_RECOMMENDATION_CREATE"].format(case_id=case_id, alert_id=alert_id) return self.validate_response(endpoint, method="POST") def case_alert_fetch_recommendation(self, case_id: str, recommendation_id: str) -> dict[str, Any]: """ Fetch a previously generated AI recommendation for a Case Alert. :type case_id: str :param case_id: The ID of the case the alert belongs to. :type recommendation_id: str :param recommendation_id: The ID of the recommendation to fetch. :return: Raw API response dict containing the recommendation details. :rtype: dict[str, Any] """ endpoint = ENDPOINTS["CASE_ALERT_FETCH_RECOMMENDATION"].format(case_id=case_id) params: dict[str, Any] = {"recommendationId": recommendation_id} return self.validate_response(endpoint, params=params) def case_alert_customfield_list( self, case_id: str, alert_id: str, page_size: int, page_token: str | None = None, order_by: str | None = None, ) -> dict[str, Any]: """ Retrieve the list of custom field values associated with a Case Alert. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type page_size: int :param page_size: Maximum number of custom field values to return. :type page_token: str | None :param page_token: Page token to resume pagination. :type order_by: str | None :param order_by: Sort field and direction, e.g. "customFieldId asc". :return: Raw API response dict containing 'customFieldValues' list and optional 'nextPageToken'. :rtype: dict[str, Any] """ params: dict[str, Any] = { "pageSize": page_size, "pageToken": page_token, "orderBy": order_by, } remove_nulls_from_dictionary(params) endpoint = ENDPOINTS["CASE_ALERT_CUSTOMFIELD_VALUES"].format(case_id=case_id, alert_id=alert_id) return self.validate_response(endpoint, params=params) def list_custom_fields( self, page_size: int, page_token: str | None = None, custom_field_filter: str | None = None, order_by: str | None = None, ) -> dict[str, Any]: """ Retrieve the list of custom fields via the customFields API. :type page_size: int :param page_size: Maximum number of custom fields to return. :type page_token: str | None :param page_token: Page token to resume pagination. :type custom_field_filter: str | None :param custom_field_filter: AIP-160 filter expression. :type order_by: str | None :param order_by: Sort field and direction, e.g. "createTime Desc". :return: Raw API response dict containing 'customFields' list and optional 'nextPageToken'. :rtype: dict[str, Any] """ params: dict[str, Any] = { "pageSize": page_size, "pageToken": page_token, "filter": custom_field_filter, "orderBy": order_by, } remove_nulls_from_dictionary(params) return self.validate_response(ENDPOINTS["CUSTOM_FIELDS"], params=params) def case_alert_involved_entities_list( self, case_id: str, alert_id: str, page_size: int, page_token: str | None = None, entity_filter: str | None = None, order_by: str | None = None, ) -> dict[str, Any]: """ Retrieve the list of involved entities for the specified Case Alert. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type page_size: int :param page_size: Maximum number of entities to return. :type page_token: str | None :param page_token: Page token to resume pagination. :type entity_filter: str | None :param entity_filter: AIP-160 filter expression. :type order_by: str | None :param order_by: Sort field and direction, e.g. "createTime Desc". :return: Raw API response dict containing 'involvedEntities' list and optional 'nextPageToken'. :rtype: dict[str, Any] """ params: dict[str, Any] = { "pageSize": page_size, "pageToken": page_token, "filter": entity_filter, "orderBy": order_by, } remove_nulls_from_dictionary(params) endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITIES"].format(case_id=case_id, alert_id=alert_id) return self.validate_response(endpoint, params=params) def case_alert_involved_entity_get(self, case_id: str, alert_id: str, entity_id: str) -> dict[str, Any]: """ Retrieve a specific InvolvedEntity by its ID. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type entity_id: str :param entity_id: The ID of the InvolvedEntity to retrieve. :return: Raw API response dict for the involved entity. :rtype: dict[str, Any] """ url_suffix = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITY"].format(case_id=case_id, alert_id=alert_id, entity_id=entity_id) return self.validate_response(url_suffix) def case_alert_involved_entity_create( self, case_id: str, alert_id: str, identifier: str, entity_type: str, suspicious: bool, internal: bool, attacker: bool | None, pivot: bool | None, operating_system: str | None, network_title: str | None, threat_source: str | None, network_priority: int | None, ) -> dict[str, Any]: """ Manually create a new InvolvedEntity within a Case Alert. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type identifier: str :param identifier: The identifier name of the entity (e.g. IP address, hostname). :type entity_type: str :param entity_type: The type of the new entity. :type suspicious: bool :param suspicious: Whether the entity is suspicious. :type internal: bool :param internal: Whether the entity is internal to the organization. :type attacker: bool | None :param attacker: Whether the entity represents an attacker. :type pivot: bool | None :param pivot: Whether the entity is a pivot entity common to multiple cases. :type operating_system: str | None :param operating_system: The operating system of the entity. :type network_title: str | None :param network_title: The network name related to the entity. :type threat_source: str | None :param threat_source: The threat source name associated with the entity. :type network_priority: int | None :param network_priority: The network priority of the entity. :return: Raw API response dict for the created involved entity. :rtype: dict[str, Any] """ endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITIES"].format(case_id=case_id, alert_id=alert_id) body: dict[str, Any] = { "identifier": identifier, "type": entity_type, "suspicious": suspicious, "internal": internal, "attacker": attacker, "pivot": pivot, "operatingSystem": operating_system, "networkTitle": network_title, "threatSource": threat_source, "networkPriority": network_priority, } remove_nulls_from_dictionary(body) return self.validate_response(endpoint, method="POST", json_data=body) def case_alert_involved_entity_update( self, case_id: str, alert_id: str, entity_id: str, suspicious: bool | None, internal: bool | None, attacker: bool | None, pivot: bool | None, operating_system: str | None, network_title: str | None, threat_source: str | None, network_priority: int | None, ) -> dict[str, Any]: """ Update an existing InvolvedEntity within a Case Alert. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type entity_id: str :param entity_id: The ID of the Involved Entity. :type suspicious: bool | None :param suspicious: Updated suspicion flag for the entity. :type internal: bool | None :param internal: Updated internal flag for the entity. :type attacker: bool | None :param attacker: Updated attacker designation for the entity. :type pivot: bool | None :param pivot: Updated pivot designation for the entity. :type operating_system: str | None :param operating_system: Updated operating system of the entity. :type network_title: str | None :param network_title: Updated network name related to the entity. :type threat_source: str | None :param threat_source: Updated threat source associated with the entity. :type network_priority: int | None :param network_priority: Updated network priority of the entity. :return: Raw API response dict for the updated involved entity. :rtype: dict[str, Any] """ endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITY"].format(case_id=case_id, alert_id=alert_id, entity_id=entity_id) body: dict[str, Any] = { "suspicious": suspicious, "internal": internal, "attacker": attacker, "pivot": pivot, "operatingSystem": operating_system, "networkTitle": network_title, "threatSource": threat_source, "networkPriority": network_priority, } remove_nulls_from_dictionary(body) return self.validate_response(endpoint, method="PATCH", json_data=body) def case_alert_involved_entity_add_property( self, case_id: str, alert_id: str, entity_id: str, key: str, value: str, ) -> dict[str, Any]: """ Add a new custom property to an InvolvedEntity within a Case Alert. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type entity_id: str :param entity_id: The ID of the Involved Entity. :type key: str :param key: The new property key. :type value: str :param value: The new property value. :return: Raw API response dict for the updated involved entity. :rtype: dict[str, Any] """ endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITY_ADD_PROPERTY"].format( case_id=case_id, alert_id=alert_id, entity_id=entity_id ) body: dict[str, Any] = {"key": key, "value": value} return self.validate_response(endpoint, method="POST", json_data=body) def case_alert_involved_entity_update_property( self, case_id: str, alert_id: str, entity_id: str, key: str, value: str, ) -> dict[str, Any]: """ Update the value of an existing custom property on an InvolvedEntity within a Case Alert. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type entity_id: str :param entity_id: The ID of the Involved Entity. :type key: str :param key: The existing property key whose value should be updated. :type value: str :param value: The new property value. :return: Raw API response dict for the updated involved entity. :rtype: dict[str, Any] """ endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITY_UPDATE_PROPERTY"].format( case_id=case_id, alert_id=alert_id, entity_id=entity_id ) body: dict[str, Any] = {"key": key, "value": value} return self.validate_response(endpoint, method="POST", json_data=body) def list_enabled_playbooks(self, case_environment: str | None, execution_scope: str) -> dict[str, Any]: """ Retrieve the list of enabled playbooks. :type case_environment: str | None :param case_environment: Case environment to filter playbooks by. :type execution_scope: str :param execution_scope: Execution scope to filter playbooks by (ALERT or CASE). :return: Raw API response dict containing 'payload' list. :rtype: dict[str, Any] """ body: dict[str, Any] = {"executionScope": execution_scope} if case_environment: body["caseEnvironment"] = case_environment return self.validate_response(ENDPOINTS["LEGACY_PLAYBOOKS"], method="POST", json_data=body) def playbook_attach( self, case_id: str, alert_group_identifier: str, alert_identifier: str, playbook_name: str, original_workflow_definition_identifier: str | None, ) -> dict[str, Any]: """ Manually attach (trigger) a specific playbook to a Case Alert. :type case_id: str :param case_id: The cyber case ID. :type alert_group_identifier: str :param alert_group_identifier: The alert group identifier. :type alert_identifier: str :param alert_identifier: The alert identifier. :type playbook_name: str :param playbook_name: The name of the playbook (workflow) to attach. :type original_workflow_definition_identifier: str | None :param original_workflow_definition_identifier: Optional original workflow definition identifier. :return: Raw API response dict. :rtype: dict[str, Any] """ endpoint = ENDPOINTS["LEGACY_PLAYBOOK_ATTACH"] body: dict[str, Any] = { "cyberCaseId": case_id, "alertGroupIdentifier": alert_group_identifier, "alertIdentifier": alert_identifier, "wfName": playbook_name, "originalWorkflowDefinitionIdentifier": original_workflow_definition_identifier, } remove_nulls_from_dictionary(body) return self.validate_response(endpoint, method="POST", json_data=body) """ HELPER FUNCTIONS """ def trim_spaces_from_args(args: dict[str, Any]) -> dict[str, Any]: """ Trim leading and trailing whitespace from all string argument values. :type args: dict[str, Any] :param args: Command arguments dictionary. :return: Arguments dictionary with string values stripped. :rtype: dict[str, Any] """ for key, value in args.items(): if isinstance(value, str): args[key] = value.strip() return args def strip_and_filter_list(list_value: list[str]) -> list[str]: """Strip whitespace from each item and drop empty values from the list.""" return [value.strip() for value in list_value if value.strip()] def check_valid_positive_number(positive_number: str) -> bool: """Check the validity of the positive integer.""" return positive_number.isdecimal() and int(positive_number) > 0 def check_empty(x: Any) -> bool: """ Check if input is empty (None, empty dict, empty list, or empty string). :type x: Any :param x: Input value to check. :return: True if x is empty, False otherwise. :rtype: bool """ return x is None or x == {} or x == [] or x == "" def remove_empty_elements_for_fetch(d: Any) -> Any: """ Recursively remove empty lists, empty dicts, or None elements from a dictionary or list. :type d: Any :param d: Dictionary or list to clean; non-collection values are returned as-is. :return: Cleaned dictionary or list with all empty elements removed. :rtype: Any """ if not isinstance(d, dict | list): return d elif isinstance(d, list): return [v for v in (remove_empty_elements_for_fetch(v) for v in d) if not check_empty(v)] return {k: v for k, v in ((k, remove_empty_elements_for_fetch(v)) for k, v in d.items()) if not check_empty(v)} def remove_empty_elements_for_hr(d: Any) -> Any: """ Recursively remove empty lists, empty dicts, or None elements from a dictionary or list. Non-collection scalar values (int, float, bool) are converted to strings so that falsy values like 0 and False are preserved in human-readable output. :type d: Any :param d: Input dictionary or list. :return: Dictionary or list with empty elements removed and scalars stringified. :rtype: Any """ if not isinstance(d, dict | list): return str(d) if isinstance(d, int | float | bool) else d elif isinstance(d, list): return [v for v in (remove_empty_elements_for_hr(v) for v in d) if not check_empty(v)] return {k: v for k, v in ((k, remove_empty_elements_for_hr(v)) for k, v in d.items()) if not check_empty(v)} def date_to_utc_epoch(date_str: str, date_format: str = DATE_FORMAT) -> int: """ Convert a date string to a UTC epoch timestamp in milliseconds. :type date_str: str :param date_str: Date string to convert. :type date_format: str :param date_format: Format used to parse date_str; defaults to DATE_FORMAT. :return: UTC epoch timestamp in milliseconds. :rtype: int """ dt = datetime.strptime(date_str, date_format) return calendar.timegm(dt.timetuple()) * 1000 + dt.microsecond // 1000 def epoch_ms_to_datestring(epoch_ms: int | str, date_format: str = DATE_FORMAT) -> str: """ Convert a UTC epoch timestamp in milliseconds to a formatted date string. :type epoch_ms: int or str :param epoch_ms: UTC epoch timestamp in milliseconds. :type date_format: str :param date_format: Output format; defaults to DATE_FORMAT. :return: Formatted UTC date string. :rtype: str """ return datetime.fromtimestamp(int(epoch_ms) / 1000, tz=UTC).strftime(date_format) def epoch_ms_to_time_delta(ms: int) -> str: """ Convert a duration in milliseconds to a human-readable string. :type ms: int :param ms: Duration in milliseconds. :return: Duration as 'X days, X hours, X minutes, X seconds'. :rtype: str """ delta = timedelta(milliseconds=ms) total_seconds = int(delta.total_seconds()) days, remainder = divmod(total_seconds, 86400) hours, remainder = divmod(remainder, 3600) minutes, seconds = divmod(remainder, 60) labels = [(days, "days"), (hours, "hours"), (minutes, "minutes"), (seconds, "seconds")] parts = [f"{value} {unit}" for value, unit in labels if value] return ", ".join(parts) or "0 seconds" def compute_cases_filter_hash( priorities: list | None, statuses: list | None, environments: list | None, tags: list | None, filter_logic: str, ) -> str: """ Compute an MD5 hash of the case filter parameters. Used to detect configuration changes between fetch cycles. If the hash differs from the stored value, the page_token and ingested_case_ids are reset to avoid a 500 error caused by a stale page_token tied to a different filter. :type priorities: list | None :param priorities: List of case priority values to filter by. :type statuses: list | None :param statuses: List of case status values to filter by. :type environments: list | None :param environments: List of case environment values to filter by. :type tags: list | None :param tags: List of case tags to filter by. :type filter_logic: str :param filter_logic: Logic operator (AND/OR) used to combine filter conditions. :return: MD5 hex digest of the sorted, serialized filter parameters. :rtype: str """ payload = { "priorities": sorted(priorities or []), "statuses": sorted(statuses or []), "environments": sorted(environments or []), "tags": sorted(tags or []), "filter_logic": filter_logic, } return hashlib.md5(json.dumps(payload, sort_keys=True).encode(), usedforsecurity=False).hexdigest() def multiline_logs_for_list(array: list, prefix: str = "") -> None: """ Log a list of items with a prefix, batched into 50 items per log message. :type array: list :param array: List of items to log. :type prefix: str :param prefix: String prepended to each log message; defaults to empty string. :return: None :rtype: None """ for b in batch(array, batch_size=50): demisto.debug(f"{prefix}{b}") def or_join(field: str, values: list) -> str: """ Build an AIP-160 OR condition for a single field across multiple values. :type field: str :param field: AIP-160 field name. :type values: list :param values: List of values to match against the field. :return: AIP-160 OR filter string, e.g. (field="value1" OR field="value2"). :rtype: str """ return "(" + " OR ".join(f'{field}="{value}"' for value in values) + ")" def any_filter(field: str, values: list) -> str: """ Build an AIP-160 any() condition for a single field across multiple values. :type field: str :param field: AIP-160 field name. :type values: list :param values: List of values to match against the field. :return: AIP-160 any() filter string, e.g. any(field,"value1","value2"). :rtype: str """ quoted = ",".join(f'"{value}"' for value in values) return f"any({field},{quoted})" def convert_time_to_ms(time_str: str, arg_name: str) -> int: """ Convert a time string (relative duration or absolute datetime) to a duration in milliseconds. Supported formats: 2 minutes, 2 hours, 2 days, 2 weeks, 2 months, 2 years, yyyy-mm-dd, yyyy-mm-ddTHH:MM:SSZ. For relative durations (e.g., "2 weeks"), returns the duration in milliseconds. For absolute datetimes, returns the milliseconds from now to that datetime. :type time_str: str :param time_str: Time string to convert. :type arg_name: str :param arg_name: The command argument name. :return: Duration in milliseconds. :rtype: int """ time_str = time_str.strip() dt = arg_to_datetime(time_str, arg_name, settings={"TIMEZONE": "UTC", "PREFER_DATES_FROM": "future"}) now = datetime.now(UTC) if dt.tzinfo is None: # type: ignore dt = dt.replace(tzinfo=UTC) # type: ignore delta_ms = int((dt - now).total_seconds() * 1000) # type: ignore if delta_ms <= 0: raise ValueError(MESSAGES["TIME_IN_PAST"].format(arg_name=arg_name, time_str=time_str)) return delta_ms def prepare_cases_filter( priorities: list | None = None, statuses: list | None = None, environments: list | None = None, tags: list | None = None, display_names: list | None = None, case_types: list | None = None, stages: list | None = None, sources: list | None = None, assignees: list | None = None, products: list | None = None, important: bool | None = None, incident: bool | None = None, workflow_statuses: list | None = None, sla_statuses: list | None = None, alerts_sla_statuses: list | None = None, filter_logic: str = DEFAULT_FILTER_LOGIC, ) -> str: """ Prepare an AIP-160 filter string from validated case filter field lists. :type priorities: list or None :param priorities: List of priority values to filter by. :type statuses: list or None :param statuses: List of status values to filter by. :type environments: list or None :param environments: List of environment values to filter by. :type tags: list or None :param tags: List of tag display names to filter by. :type display_names: list or None :param display_names: List of display name values to filter by. :type case_types: list or None :param case_types: List of case type values to filter by. :type stages: list or None :param stages: List of stage values to filter by. :type sources: list or None :param sources: List of source values to filter by. :type assignees: list or None :param assignees: List of assignee values to filter by. :type products: list or None :param products: List of product display names to filter by. :type important: bool or None :param important: Filter by important flag. :type incident: bool or None :param incident: Filter by incident flag. :type workflow_statuses: list or None :param workflow_statuses: List of workflow status values to filter by. :type sla_statuses: list or None :param sla_statuses: List of SLA status values to filter by. :type alerts_sla_statuses: list or None :param alerts_sla_statuses: List of alerts SLA status values to filter by. :type filter_logic: str :param filter_logic: Logical operator used to join conditions — "AND" or "OR". :return: AIP-160 filter string, or empty string if no filters are set. :rtype: str """ # Separator placed between each condition, e.g. " AND " or " OR " join_separator = f" {filter_logic.upper()} " conditions = [] # Fields that support equality matching across multiple values via OR e.g. (field="a" OR field="b") field_value_pairs: list[tuple[str, list | None]] = [ ("priority", priorities), ("status", statuses), ("environment", environments), ("displayName", display_names), ("type", case_types), ("stage", stages), ("source", sources), ("assignee", assignees), ("workflowStatus", workflow_statuses), ("sla.expirationStatus", sla_statuses), ("alertsSla.expirationStatus", alerts_sla_statuses), ] # Build an OR expression for each field that has values provided for field, values in field_value_pairs: if values: conditions.append(or_join(field, values)) # tags and products use AIP-160 any() because they are repeated fields on the case if tags: conditions.append(any_filter("tags.displayName", tags)) if products: conditions.append(any_filter("products.displayName", products)) # Boolean flags are scalar fields — emit as-is with lowercase true/false if important is not None: conditions.append(f"important={str(important).lower()}") if incident is not None: conditions.append(f"incident={str(incident).lower()}") return join_separator.join(conditions) def prepare_cases_date_filter( create_start_time: str | None = None, create_end_time: str | None = None, update_start_time: str | None = None, update_end_time: str | None = None, ) -> str: """ Prepare an AIP-160 filter string from date/time filter fields. Date conditions are always joined with AND. :type create_start_time: str or None :param create_start_time: Lower bound for case create time. :type create_end_time: str or None :param create_end_time: Upper bound for case create time. :type update_start_time: str or None :param update_start_time: Lower bound for case update time. :type update_end_time: str or None :param update_end_time: Upper bound for case update time. :return: AIP-160 filter string, or empty string if no date filters are set. :rtype: str """ conditions = [] if create_start_time: conditions.append(f"createTime>={date_to_utc_epoch(create_start_time)}") if create_end_time: conditions.append(f"createTime<={date_to_utc_epoch(create_end_time)}") if update_start_time: conditions.append(f"updateTime>={date_to_utc_epoch(update_start_time)}") if update_end_time: conditions.append(f"updateTime<={date_to_utc_epoch(update_end_time)}") return " AND ".join(conditions) def prepare_alert_entity_filter( entity_types: list[str] | None = None, suspicious: bool | None = None, internal: bool | None = None, attacker: bool | None = None, pivot: bool | None = None, enriched: bool | None = None, artifact: bool | None = None, vulnerable: bool | None = None, manually_created: bool | None = None, threat_sources: list[str] | None = None, operating_systems: list[str] | None = None, network_titles: list[str] | None = None, network_priorities: list[str] | None = None, environments: list[str] | None = None, filter_logic: str = DEFAULT_FILTER_LOGIC, ) -> str: """ Prepare an AIP-160 filter string for involved entities. :type filter_logic: str :param filter_logic: Logical operator used to join conditions — "AND" or "OR". :return: AIP-160 filter string, or empty string if no filters are set. :rtype: str """ join_separator = f" {filter_logic.upper()} " conditions = [] if entity_types: conditions.append(or_join("type", entity_types)) bool_fields: list[tuple[str, bool | None]] = [ ("suspicious", suspicious), ("internal", internal), ("attacker", attacker), ("pivot", pivot), ("enriched", enriched), ("artifact", artifact), ("vulnerable", vulnerable), ("manuallyCreated", manually_created), ] for field, value in bool_fields: if value is not None: conditions.append(f"{field}={str(value).lower()}") string_list_fields: list[tuple[str, list | None]] = [ ("threatSource", threat_sources), ("operatingSystem", operating_systems), ("networkTitle", network_titles), ("environment", environments), ] for field, values in string_list_fields: if values: conditions.append(or_join(field, values)) if network_priorities: conditions.append("(" + " OR ".join(f"networkPriority={v}" for v in network_priorities) + ")") return join_separator.join(conditions) def prepare_sla_hr(sla: dict[str, Any]) -> dict[str, Any]: """ Format an SLA dict for human-readable output. Converts unix-millisecond timestamp fields (expirationTime, criticalExpirationTime) to ISO 8601 strings via timestamp_to_datestring. Renames keys to display-friendly labels. :type sla: dict[str, Any] :param sla: Raw SLA object from the Google SecOps API response. :return: Dict with display-friendly keys and formatted timestamp values. :rtype: dict[str, Any] """ result: dict[str, Any] = { "Status": sla.get("expirationStatus"), } expiration_time = sla.get("expirationTime") if expiration_time: result["Expiration Time"] = epoch_ms_to_datestring(expiration_time, date_format=HR_DATE_FORMAT) critical_expiration_time = sla.get("criticalExpirationTime") if critical_expiration_time: result["Critical Expiration Time"] = epoch_ms_to_datestring(critical_expiration_time, date_format=HR_DATE_FORMAT) remaining_time = sla.get("remainingTimeSinceLastPause") if remaining_time: result["Remaining Time Since Last Pause"] = epoch_ms_to_time_delta(remaining_time) return result def prepare_context_hr_gcb_case_list(response: dict[str, Any]) -> tuple[dict[str, Any], str]: """ Prepare context and human-readable output for case list. :type response: dict :param response: Response dictionary from API response. :return: Tuple of context dict and human-readable markdown string. :rtype: tuple[dict[str, Any], str] """ cases: list[dict[str, Any]] = deepcopy(response.get("cases", [])) next_page_token = response.get("nextPageToken", "") table_data: list[dict[str, Any]] = [] for case in cases: parts = case.get("name", "").split("cases/") case_id = parts[1] if len(parts) >= 2 else "" case["caseId"] = case_id priority = case.get("priority", "") display_priority = priority.replace("PRIORITY_", "") if priority else "" create_time = case.get("createTime") if create_time: create_time = epoch_ms_to_datestring(create_time, date_format=HR_DATE_FORMAT) table_data.append( { CASE_ID_DISPLAY: case_id, "Display Name": case.get("displayName", ""), "Priority": display_priority, "Status": case.get("status", ""), "Score": case.get("score", ""), "Stage": case.get("stage", ""), "Environment": case.get("environment", ""), "Workflow Status": case.get("workflowStatus", ""), "Assignee": case.get("assignee", ""), "Tags": case.get("tags", ""), "SLA": prepare_sla_hr(case.get("sla", {})), "Alert Count": case.get("alertCount", ""), "Create Time": create_time, } ) readable_output = tableToMarkdown( "Case List", remove_empty_elements_for_hr(table_data), headers=[ CASE_ID_DISPLAY, "Display Name", "Priority", "Status", "Score", "Stage", "Environment", "Workflow Status", "Assignee", "Tags", "SLA", "Alert Count", "Create Time", ], removeNull=True, is_auto_json_transform=True, ) if next_page_token: readable_output += ( f"\nMaximum number of cases specified in page_size has been returned. " f"To fetch the next set of cases, execute the command with the page token as `{next_page_token}`." ) outputs: dict[str, Any] = {} outputs[f"{SECOPS_OUTPUT_PATHS['Case']}(val.caseId == obj.caseId)"] = cases if next_page_token: outputs[f"{SECOPS_OUTPUT_PATHS['PageToken']}(val.command == obj.command)"] = { "command": "gcb-case-list", "nextPageToken": next_page_token, "totalSize": response.get("totalSize"), } return outputs, readable_output def resolve_assignees(client: Client, assignees: list[str], raise_on_not_found: bool = False) -> list[str]: """ Resolve a list of assignee values to their SOAR user IDs or pass through SOC roles. :type client: Client :param client: Client instance used to resolve emails to user IDs. :type assignees: list[str] :param assignees: List of email addresses or SOC roles (e.g. @Tier1). :type raise_on_not_found: bool :param raise_on_not_found: If True, raises ValueError when an email cannot be resolved. If False, unresolved emails are skipped and logged. :return: Resolved list of user IDs or SOC roles. :rtype: list[str] """ resolved, not_found, emails = [], [], [] for assignee in assignees: if not assignee.startswith("@"): emails.append(assignee) else: resolved.append(assignee) email_to_id: dict[str, str] = {} if emails: email_to_id = client.get_soar_user_ids(emails) for email in emails: resolved_id = email_to_id.get(email.lower()) if not resolved_id: if raise_on_not_found: raise ValueError(MESSAGES["USER_NOT_FOUND"].format(email)) not_found.append(email) continue resolved.append(resolved_id) demisto.debug(f"resolve_assignees: resolved '{email}' -> '{resolved_id}'.") if not_found: demisto.debug(f"resolve_assignees: no active user found for emails {not_found}, skipping.") return resolved def prepare_context_hr_gcb_case_get_update( response: dict[str, Any], title: str = "Case Information", case_id: str = "" ) -> tuple[dict[str, Any], str]: """ Prepare context and human readable output for a single case response. Used by both gcb-case-get and gcb-case-update commands. :type response: dict[str, Any] :param response: Raw API response for a single case. :type title: str :param title: Title for the human-readable table. :type case_id: str :param case_id: Case ID to use when the response does not contain a ``name`` field. :return: Tuple of (context dict, human readable markdown string). :rtype: tuple[dict[str, Any], str] """ parts = response.get("name", "").split("cases/") case_id = parts[1] if len(parts) >= 2 else case_id context: dict[str, Any] = response.copy() context["caseId"] = case_id context = remove_empty_elements(context) sla = prepare_sla_hr(response.get("sla", {})) alerts_sla = prepare_sla_hr(response.get("alertsSla", {})) priority = response.get("priority", "") display_priority = priority.replace("PRIORITY_", "") if priority else "" create_time = response.get("createTime") if create_time: create_time = epoch_ms_to_datestring(create_time, date_format=HR_DATE_FORMAT) update_time = response.get("updateTime") if update_time: update_time = epoch_ms_to_datestring(update_time, date_format=HR_DATE_FORMAT) raw_hr: dict[str, Any] = deepcopy(response) raw_hr["caseId"] = case_id raw_hr["sla"] = sla raw_hr["alertsSla"] = alerts_sla raw_hr["priority"] = display_priority raw_hr["createTime"] = create_time raw_hr["updateTime"] = update_time hr_data = remove_empty_elements_for_hr(raw_hr) headers = [ "caseId", "displayName", "priority", "status", "stage", "assignee", "alertCount", "type", "environment", "source", "workflowStatus", "description", "sla", "alertsSla", "tags", "createTime", "updateTime", "score", "incident", "important", "involvedSuspiciousEntity", "overflowCase", "creatorUserId", "lastModifyingUserId", "products", "tasks", "closureDetails", ] readable_output = tableToMarkdown( title, hr_data, headers=headers, removeNull=True, sort_headers=False, is_auto_json_transform=True, headerTransform=lambda f: re.sub(r"\bId\b", "ID", pascalToSpace(f)).replace("Sla", "SLA"), ) return context, readable_output def prepare_context_hr_case_comment_list(response: dict[str, Any]) -> tuple[dict[str, Any], str]: """ Prepare context and human readable output for case comment list. :type response: ``dict`` :param response: Response dictionary from API response. :return: Tuple of context and human readable output. """ case_comments = deepcopy(response.get("caseComments", [])) table_data: list[dict[str, Any]] = [] for comment in case_comments: create_time_str = "" if comment.get("createTime"): create_time_ms = comment["createTime"] create_time_str = epoch_ms_to_datestring(create_time_ms, HR_DATE_FORMAT) author = comment.get("userOwnerFullName") or "Automation" table_data.append( { "Author": author, "Comment": comment.get("comment", ""), "Create Time": create_time_str, } ) comment["commentId"] = str(comment.get("name", "")).rpartition("/")[2] readable_output = tableToMarkdown("Case Comments", table_data, headers=["Author", "Comment", "Create Time"], removeNull=True) next_page_token: str | None = response.get("nextPageToken") if next_page_token: readable_output += ( f"\nMaximum number of comments specified in page_size has been returned. " f"To fetch the next set of comments, execute the command with the page token as `{next_page_token}`." ) outputs: dict[str, Any] = {} outputs[f"{SECOPS_OUTPUT_PATHS['CaseComment']}(val.name == obj.name)"] = case_comments if next_page_token: outputs[f"{SECOPS_OUTPUT_PATHS['PageToken']}(val.command == obj.command)"] = { "command": "gcb-case-comment-list", "nextPageToken": next_page_token, "totalSize": response.get("totalSize"), } return remove_empty_elements(outputs), readable_output def prepare_context_hr_case_comment_create(response: dict[str, Any]) -> tuple[dict[str, Any], str]: """ Prepare context and human readable output for case comment create. :type response: ``dict`` :param response: Response dictionary from API response. :return: Tuple of context and human readable output. """ comment_data = deepcopy(response) create_time_hr = "" if comment_data.get("createTime"): create_time_ms = comment_data["createTime"] create_time_hr = epoch_ms_to_datestring(create_time_ms, date_format=HR_DATE_FORMAT) comment_data["commentId"] = str(comment_data.get("name", "")).rpartition("/")[2] case_id = comment_data.get("case", "") comment_text = comment_data.get("comment", "") readable_output = f'Successfully added the following comment to case "{case_id}" at {create_time_hr}:\n\n`{comment_text}`' return remove_empty_elements(comment_data), readable_output def prepare_case_alerts_filter( display_names: list | None = None, priorities: list | None = None, statuses: list | None = None, products: list | None = None, vendors: list | None = None, environments: list | None = None, source_system_names: list | None = None, tags: list | None = None, manual: bool | None = None, filter_logic: str = DEFAULT_FILTER_LOGIC, ) -> str: """ Prepare an AIP-160 filter string from case alert filter field lists. :type display_names: list or None :param display_names: List of display name values to filter by. :type priorities: list or None :param priorities: List of priority values to filter by. :type statuses: list or None :param statuses: List of status values to filter by. :type products: list or None :param products: List of product name values to filter by. :type vendors: list or None :param vendors: List of vendor name values to filter by. :type environments: list or None :param environments: List of environment values to filter by. :type source_system_names: list or None :param source_system_names: List of source system name values to filter by. :type tags: list or None :param tags: List of tag values to filter by. :type manual: bool or None :param manual: Filter by whether alerts were created manually. :type filter_logic: str :param filter_logic: Logical operator used to join conditions — "AND" or "OR". :return: AIP-160 filter string, or empty string if no filters are set. :rtype: str """ join_separator = f" {filter_logic.upper()} " conditions = [] field_value_pairs: list[tuple[str, list | None]] = [ ("displayName", display_names), ("priority", priorities), ("status", statuses), ("product", products), ("vendor", vendors), ("environment", environments), ("sourceSystemName", source_system_names), ] for field, values in field_value_pairs: if values: conditions.append(or_join(field, values)) if tags: conditions.append(any_filter("tags.tag", tags)) if manual is not None: conditions.append(f"manual={str(manual).lower()}") return join_separator.join(conditions) def prepare_case_alerts_date_filter( create_start_time: str | None = None, create_end_time: str | None = None, update_start_time: str | None = None, update_end_time: str | None = None, ) -> str: """ Prepare an AIP-160 filter string from case alert date/time fields. Date conditions are always joined with AND. :type create_start_time: str or None :param create_start_time: Lower bound for alert create time. :type create_end_time: str or None :param create_end_time: Upper bound for alert create time. :type update_start_time: str or None :param update_start_time: Lower bound for alert update time. :type update_end_time: str or None :param update_end_time: Upper bound for alert update time. :return: AIP-160 filter string, or empty string if no date filters are set. :rtype: str """ conditions = [] if create_start_time: conditions.append(f"createTime>={date_to_utc_epoch(create_start_time)}") if create_end_time: conditions.append(f"createTime<={date_to_utc_epoch(create_end_time)}") if update_start_time: conditions.append(f"updateTime>={date_to_utc_epoch(update_start_time)}") if update_end_time: conditions.append(f"updateTime<={date_to_utc_epoch(update_end_time)}") return " AND ".join(conditions) def prepare_context_hr_gcb_case_alert_list(response: dict[str, Any]) -> tuple[dict[str, Any], str]: """ Prepare context and human readable output for gcb-case-alert-list. :type response: dict[str, Any] :param response: Raw API response containing 'caseAlerts' list. :return: Tuple of (outputs dict, human readable markdown string). :rtype: tuple[dict[str, Any], str] """ case_alerts: list[dict[str, Any]] = deepcopy(response.get("caseAlerts", [])) next_page_token = response.get("nextPageToken", "") total_size = response.get("totalSize") table_data: list[dict[str, Any]] = [] for alert in case_alerts: parts = alert.get("name", "").split("caseAlerts/") alert["alertId"] = parts[1] if len(parts) >= 2 else "" create_time = alert.get("createTime") if create_time: create_time = epoch_ms_to_datestring(create_time, date_format=HR_DATE_FORMAT) table_data.append( { ALERT_ID_DISPLAY: alert.get("alertId", ""), "Alert Name": alert.get("displayName", ""), "Create Time": create_time, "Priority": alert.get("priority", ""), "Status": alert.get("status", ""), "Events Count": alert.get("eventCount", ""), "Alert SLA": prepare_sla_hr(alert.get("sla", {})), "Playbook Attached Name": alert.get("attachedPlaybookName", ""), "Playbook Attached Status": alert.get("playbookStatus", ""), } ) readable_output = tableToMarkdown( "Case Alerts List", remove_empty_elements_for_hr(table_data), headers=[ ALERT_ID_DISPLAY, "Alert Name", "Create Time", "Priority", "Status", "Events Count", "Alert SLA", "Playbook Attached Name", "Playbook Attached Status", ], removeNull=True, is_auto_json_transform=True, ) if next_page_token: readable_output += ( f"\nMaximum number of alerts specified in page_size has been returned. " f"To fetch the next set of alerts, execute the command with the page token as `{next_page_token}`." ) outputs: dict[str, Any] = {} outputs[f"{SECOPS_OUTPUT_PATHS['CaseAlert']}(val.alertId == obj.alertId)"] = case_alerts if next_page_token: outputs[f"{SECOPS_OUTPUT_PATHS['PageToken']}(val.command == obj.command)"] = { "command": "gcb-case-alert-list", "nextPageToken": next_page_token, "totalSize": total_size, } return remove_empty_elements(outputs), readable_output def prepare_context_hr_gcb_case_alert_get_update( response: dict[str, Any], title: str = "Case Alert Information", alert_id: str = "" ) -> tuple[dict[str, Any], str]: """ Prepare context and human readable output for gcb-case-alert-get and gcb-case-alert-update. :type response: dict[str, Any] :param response: Raw API response for a single Case Alert. :type title: str :param title: Title for the human-readable table. :type alert_id: str :param alert_id: Alert ID to use when the response does not contain a ``name`` field. :return: Tuple of (context dict, human readable markdown string). :rtype: tuple[dict[str, Any], str] """ context = deepcopy(response) parts = response.get("name", "").split("caseAlerts/") alert_id = parts[1] if len(parts) >= 2 else alert_id context["alertId"] = alert_id tags = [t.get("tag") for t in response.get("tags", []) if t.get("tag")] raw_hr: dict[str, Any] = deepcopy(context) raw_hr["sla"] = prepare_sla_hr(response.get("sla", {})) for field in ("createTime", "updateTime", "startTime", "endTime"): val = raw_hr.get(field) raw_hr[field] = epoch_ms_to_datestring(val, date_format=HR_DATE_FORMAT) if val else None raw_hr["tags"] = ", ".join(tags) raw_hr["manual"] = response.get("manual", False) raw_hr["alertIdentifier"] = response.get("identifier", "") hr_data = remove_empty_elements_for_hr(raw_hr) headers = [ "alertId", "caseId", "displayName", "status", "priority", "product", "vendor", "environment", "tags", "sla", "eventCount", "alertIdentifier", "alertGroupIdentifier", "playbookStatus", "attachedPlaybookName", "playbookRunCount", "createTime", "updateTime", "startTime", "endTime", "manual", "ruleGenerator", "ticketId", "sourceSystemName", "sourceIdentifier", "sourceGroupingIdentifier", "siemAlertId", "sourceUrl", "additionalProperties", "closureDetails", "involvedRelations", ] readable_output = tableToMarkdown( title, hr_data, headers=headers, removeNull=True, sort_headers=False, is_auto_json_transform=True, headerTransform=lambda f: re.sub(r"\bId\b", "ID", pascalToSpace(f)).replace("Sla", "SLA").replace("Url", "URL"), ) return remove_empty_elements(context), readable_output def prepare_context_hr_gcb_case_alert_recommendation_fetch( response: dict[str, Any], recommendation_id: str ) -> tuple[dict[str, Any], str]: """ Prepare context and human readable output for gcb-case-alert-recommendation-fetch. :type response: dict[str, Any] :param response: Raw API response from fetchRecommendation. :type recommendation_id: str :param recommendation_id: The ID of the recommendation. :return: Tuple of (context dict, human readable markdown string). :rtype: tuple[dict[str, Any], str] """ context = deepcopy(response) context["recommendationId"] = recommendation_id hr_lines = ["### Alert Recommendation"] if state := response.get("state"): hr_lines.append(f"**State:** {state}") if recommendation := response.get("recommendation"): hr_lines.append(f"**Recommendation:**\n{recommendation}") if alert_id_to_case_id := response.get("alertIdentifierToCaseId"): formatted = "\n".join(f"- `{alert_id}`: {case_id}" for alert_id, case_id in alert_id_to_case_id.items()) hr_lines.append(f"**Alert Identifier To Case ID:**\n{formatted}") if marketplace_actions := response.get("marketplaceActionsTriggeredManually"): hr_lines.append(f"**Marketplace Actions Triggered Manually:** {', '.join(marketplace_actions)}") readable_output = "\n\n---\n\n".join(hr_lines) return remove_empty_elements(context), readable_output def prepare_context_hr_gcb_case_alert_customfield_list( response: dict[str, Any], id_to_display: dict[str, str] ) -> tuple[dict[str, Any], str]: """ Prepare context and human readable output for gcb-case-alert-customfield-list. :type response: dict :param response: Response dictionary from the customFieldValues API. :type id_to_display: dict[str, str] :param id_to_display: Mapping of custom field ID to display name from the customFields API. :return: Tuple of context outputs and human readable markdown string. :rtype: tuple[dict[str, Any], str] """ custom_field_values: list[dict[str, Any]] = deepcopy(response.get("customFieldValues", [])) hr_rows = [] for cfv in custom_field_values: field_id = cfv.get("customFieldId", "") cfv["displayName"] = id_to_display.get(field_id, "") row = { "customFieldId": field_id, "displayName": cfv.get("displayName", ""), "values": ", ".join(cfv.get("values", [])), "valuesSearchText": cfv.get("valuesSearchText", ""), } hr_rows.append(row) headers = ["customFieldId", "displayName", "values", "valuesSearchText"] readable_output = tableToMarkdown( "Case Alert Custom Field Values", hr_rows, headers=headers, removeNull=True, sort_headers=False, headerTransform=lambda f: re.sub(r"\bId\b", "ID", pascalToSpace(f)), ) next_page_token: str | None = response.get("nextPageToken") if next_page_token: readable_output += ( f"\nMaximum number of custom field values specified in page_size has been returned. " f"To fetch the next set of custom field values, execute the command with the page token as `{next_page_token}`." ) outputs: dict[str, Any] = {} outputs[f"{SECOPS_OUTPUT_PATHS['AlertCustomFieldValue']}(val.customFieldId == obj.customFieldId)"] = custom_field_values if next_page_token: outputs[f"{SECOPS_OUTPUT_PATHS['PageToken']}(val.command == obj.command)"] = { "command": "gcb-case-alert-customfield-list", "nextPageToken": next_page_token, "totalSize": response.get("totalSize"), } return remove_empty_elements(outputs), readable_output def prepare_context_hr_case_alert_entity_list(response: dict[str, Any]) -> tuple[dict[str, Any], str]: """ Prepare context and human readable output for alert entity list. :type response: ``dict`` :param response: Response dictionary from API response. :return: Tuple of context and human readable output. :rtype: tuple[dict[str, Any], str] """ entities = deepcopy(response.get("involvedEntities", [])) hr_rows = [] for entity in entities: hr_rows.append(remove_empty_elements_for_hr(entity)) headers = [ "id", "identifier", "type", "environment", "suspicious", "internal", "attacker", "pivot", "enriched", "artifact", "vulnerable", "manuallyCreated", "threatSource", "operatingSystem", "networkTitle", "networkPriority", "entityUri", "sourceSystemUri", ] readable_output = tableToMarkdown( "Alert Entities List", hr_rows, headers=headers, removeNull=True, sort_headers=False, is_auto_json_transform=True, headerTransform=lambda f: " ".join("ID" if p == "Id" else "URI" if p == "Uri" else p for p in pascalToSpace(f).split()), ) next_page_token: str | None = response.get("nextPageToken") if next_page_token: readable_output += ( f"\nMaximum number of entities specified in page_size has been returned. " f"To fetch the next set of entities, execute the command with the page token as `{next_page_token}`." ) outputs: dict[str, Any] = {} outputs[f"{SECOPS_OUTPUT_PATHS['AlertEntity']}(val.id == obj.id)"] = entities if next_page_token: outputs[f"{SECOPS_OUTPUT_PATHS['PageToken']}(val.command == obj.command)"] = { "command": "gcb-case-alert-entity-list", "nextPageToken": next_page_token, "totalSize": response.get("totalSize"), } return remove_empty_elements(outputs), readable_output def prepare_context_hr_case_alert_entity_get( response: dict[str, Any], title: str = "Entity Information", alert_id: str = "" ) -> tuple[dict[str, Any], str]: """ Prepare context and human readable output for a single alert entity. :type response: ``dict`` :param response: Response dictionary from API response. :type title: str :param title: Title for the human-readable table. :type alert_id: str :param alert_id: Alert ID the entity belongs to. :return: Tuple of context and human readable output. :rtype: tuple[dict[str, Any], str] """ entity = deepcopy(response) entity["alertId"] = alert_id entity = remove_empty_elements(entity) hr_row = remove_empty_elements_for_hr(deepcopy(entity)) headers = [ "id", "identifier", "alertId", "caseId", "alertIdentifier", "type", "environment", "suspicious", "internal", "attacker", "pivot", "enriched", "artifact", "vulnerable", "manuallyCreated", "threatSource", "operatingSystem", "networkTitle", "networkPriority", "entityUri", "sourceSystemUri", "additionalProperties", "fields", ] readable_output = tableToMarkdown( title, hr_row, headers=headers, removeNull=True, sort_headers=False, is_auto_json_transform=True, headerTransform=lambda f: " ".join("ID" if p == "Id" else "URI" if p == "Uri" else p for p in pascalToSpace(f).split()), ) return remove_empty_elements(entity), readable_output def prepare_context_hr_gcb_playbook_list(response: dict[str, Any]) -> tuple[list[dict[str, Any]], str]: """ Prepare context and human readable output for enabled playbook list. :type response: ``dict`` :param response: Response dictionary from API response. :return: Tuple of context list and human readable output. :rtype: tuple[list[dict[str, Any]], str] """ playbooks = deepcopy(response.get("payload", [])) headers = [ "playbookName", "description", "playbookType", "originalWorkflowDefinitionIdentifier", "workflowDefinitionIdentifier", "isDebugMode", ] readable_output = tableToMarkdown( "Enabled Playbooks", remove_empty_elements_for_hr(playbooks), headers=headers, removeNull=True, sort_headers=False, is_auto_json_transform=True, headerTransform=pascalToSpace, ) return remove_empty_elements(playbooks), readable_output """ VALIDATION FUNCTIONS """ def validate_argument(value: Any, name: str) -> Any: """ Check if empty value is passed for an argument and raise appropriate ValueError. :type value: Any :param value: Value of the argument. :type name: str :param name: Name of the argument. """ if not value: raise ValueError(MESSAGES["REQUIRED_ARGUMENT"].format(name)) return value def validate_configuration_parameters(params: dict[str, Any]): """ Check whether entered configuration parameters are valid or not. :type params: dict :param params: Dictionary of demisto configuration parameter :return: raise ValueError if any configuration parameter is not in valid format else returns None :rtype: None """ service_account_json = params.get("credentials", {}).get("password", "") project_instance_id = params.get("secops_project_instance_id", "") project_location = params.get("region", DEFAULT_REGION).lower() if project_location == "other": project_location = params.get("other_region", "").lower() project_number = params.get("secops_project_number", "") if not project_instance_id: raise ValueError(MESSAGES["MISSING_PROJECT_INSTANCE_ID"]) if not project_location: raise ValueError(MESSAGES["MISSING_REGION"]) if project_number and not check_valid_positive_number(project_number): raise ValueError(MESSAGES["INVALID_PROJECT_NUMBER"]) try: json.loads(service_account_json, strict=False) except json.decoder.JSONDecodeError: raise ValueError(MESSAGES["INVALID_SERVICE_ACCOUNT_JSON"]) def validate_positive_integer_list(values: list[str], arg_name: str, display_name: str) -> list[str]: """ Validate that all values in the list are positive integers. :type values: list[str] :param values: List of string values to validate. :type arg_name: str :param arg_name: Argument name of the field being validated. :type display_name: str :param display_name: Human-readable singular label for the field. :return: The original list if all values are valid. :rtype: list[str] """ values = strip_and_filter_list(values) validate_argument(values, arg_name) invalid_values = [value for value in values if not check_valid_positive_number(value)] if invalid_values: raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format(arg_name, ", ".join(invalid_values), display_name)) return values def validate_date_range( start_time: Optional[datetime], start_time_arg: str, end_time: Optional[datetime], end_time_arg: str, ) -> None: """ Validate that start time is not greater than end time. :type start_time: Optional[datetime] :param start_time: The start time value. :type start_time_arg: str :param start_time_arg: The argument name for start time (used in error messages). :type end_time: Optional[datetime] :param end_time: The end time value. :type end_time_arg: str :param end_time_arg: The argument name for end time (used in error messages). :raises ValueError: If start time is greater than end time. :return: None :rtype: None """ if start_time and end_time and start_time > end_time: raise ValueError(MESSAGES["INVALID_DATE_RANGE"].format(start_time_arg, end_time_arg)) def validate_case_list_date_args( create_start_time: Optional[datetime], create_end_time: Optional[datetime], update_start_time: Optional[datetime], update_end_time: Optional[datetime], ) -> tuple[str, str, str, str]: """ Validate date range arguments for gcb-case-list command and return formatted strings. :type create_start_time: Optional[datetime] :param create_start_time: The create start time value. :type create_end_time: Optional[datetime] :param create_end_time: The create end time value. :type update_start_time: Optional[datetime] :param update_start_time: The update start time value. :type update_end_time: Optional[datetime] :param update_end_time: The update end time value. :raises ValueError: If start time is greater than end time. :return: Formatted (create_start_time, create_end_time, update_start_time, update_end_time) strings or empty string. :rtype: tuple """ validate_date_range(create_start_time, "create_start_time", create_end_time, "create_end_time") validate_date_range(update_start_time, "update_start_time", update_end_time, "update_end_time") return ( create_start_time.strftime(DATE_FORMAT) if create_start_time else "", create_end_time.strftime(DATE_FORMAT) if create_end_time else "", update_start_time.strftime(DATE_FORMAT) if update_start_time else "", update_end_time.strftime(DATE_FORMAT) if update_end_time else "", ) def validate_case_alert_list_date_args( create_start_time: Optional[datetime], create_end_time: Optional[datetime], update_start_time: Optional[datetime] = None, update_end_time: Optional[datetime] = None, ) -> tuple[str, str, str, str]: """ Validate date range arguments for gcb-case-alert-list command and return formatted strings. :type create_start_time: Optional[datetime] :param create_start_time: The create start time value. :type create_end_time: Optional[datetime] :param create_end_time: The create end time value. :type update_start_time: Optional[datetime] :param update_start_time: The update start time value. :type update_end_time: Optional[datetime] :param update_end_time: The update end time value. :raises ValueError: If start time is greater than end time. :return: Formatted (create_start_time, create_end_time, update_start_time, update_end_time) strings or empty string. :rtype: tuple """ validate_date_range(create_start_time, "create_start_time", create_end_time, "create_end_time") validate_date_range(update_start_time, "update_start_time", update_end_time, "update_end_time") return ( create_start_time.strftime(DATE_FORMAT) if create_start_time else "", create_end_time.strftime(DATE_FORMAT) if create_end_time else "", update_start_time.strftime(DATE_FORMAT) if update_start_time else "", update_end_time.strftime(DATE_FORMAT) if update_end_time else "", ) def validate_case_list_args( page_size: int, sort_by: str, sort_order: str, priorities: list[str], statuses: list[str], case_types: list[str], workflow_statuses: list[str], sla_statuses: list[str], alerts_sla_statuses: list[str], filter_logic: str = DEFAULT_FILTER_LOGIC, ) -> tuple[int, str, str, list[str], list[str], list[str], list[str], list[str], list[str], str]: """ Validate arguments for gcb-case-list command. :type page_size: int :param page_size: Number of cases to return per page. :type sort_by: str :param sort_by: Field to sort results by. :type sort_order: str :param sort_order: Sort direction ('Asc' or 'Desc'). :type priorities: list[str] :param priorities: List of priority filter values. :type statuses: list[str] :param statuses: List of status filter values. :type case_types: list[str] :param case_types: List of case type filter values. :type workflow_statuses: list[str] :param workflow_statuses: List of workflow status filter values. :type sla_statuses: list[str] :param sla_statuses: List of SLA status filter values. :type alerts_sla_statuses: list[str] :param alerts_sla_statuses: List of alerts SLA status filter values. :type filter_logic: str :param filter_logic: Logical operator to combine filter conditions ('AND' or 'OR'). :raises ValueError: If any argument fails validation. :return: Validated (page_size, sort_by, sort_order, priorities, statuses, case_types, workflow_statuses, sla_statuses, alerts_sla_statuses, filter_logic) :rtype: tuple """ if page_size < 1 or page_size > MAX_PAGE_SIZE: raise ValueError(MESSAGES["INVALID_INT_RANGE"].format(page_size, "page_size", 1, MAX_PAGE_SIZE)) sort_order = sort_order.title() if sort_order not in VALID_SORT_ORDERS: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("sort_order", ", ".join(VALID_SORT_ORDERS))) validated_priorities = [] for priority in priorities: if priority.upper() not in VALID_CASE_PRIORITIES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("priority", ", ".join(VALID_CASE_PRIORITIES))) validated_priorities.append(f"PRIORITY_{priority.upper()}") validated_statuses = [] for status in statuses: if status.upper() not in VALID_CASE_STATUSES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("status", ", ".join(VALID_CASE_STATUSES))) validated_statuses.append(status.upper()) validated_case_types = [] for case_type in case_types: if case_type.upper() not in VALID_CASE_TYPES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("type", ", ".join(VALID_CASE_TYPES))) validated_case_types.append(case_type.upper()) validated_workflow_statuses = [] for workflow_status in workflow_statuses: if workflow_status.upper() not in VALID_CASE_WORKFLOW_STATUSES: raise ValueError( MESSAGES["VALIDATE_SINGLE_SELECT"].format("workflow_status", ", ".join(VALID_CASE_WORKFLOW_STATUSES)) ) validated_workflow_statuses.append(workflow_status.upper()) validated_sla_statuses = [] for sla_status in sla_statuses: if sla_status.upper() not in VALID_CASE_SLA_STATUSES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("sla", ", ".join(VALID_CASE_SLA_STATUSES))) validated_sla_statuses.append(sla_status.upper()) validated_alerts_sla_statuses = [] for alerts_sla_status in alerts_sla_statuses: if alerts_sla_status.upper() not in VALID_CASE_SLA_STATUSES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("alerts_sla", ", ".join(VALID_CASE_SLA_STATUSES))) validated_alerts_sla_statuses.append(alerts_sla_status.upper()) if filter_logic.upper() not in VALID_CASE_FILTER_LOGIC: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("filter_logic", ", ".join(VALID_CASE_FILTER_LOGIC))) return ( page_size, sort_by, sort_order, validated_priorities, validated_statuses, validated_case_types, validated_workflow_statuses, validated_sla_statuses, validated_alerts_sla_statuses, filter_logic, ) def validate_case_update_args( case_id: str, display_name: str, description: str, important: bool | None, incident: bool | None, ) -> tuple[dict[str, Any], str]: """ Validate arguments for gcb-case-update and build the request body and updateMask. :type case_id: str :param case_id: Case ID to update. :type display_name: str :param display_name: Display name of the case. :type description: str :param description: Description of the case. :type important: bool or None :param important: Whether the case is marked as important. :type incident: bool or None :param incident: Whether the case is marked as an incident. :return: Tuple of (body dict, updateMask string). :rtype: tuple[dict[str, Any], str] """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not any([display_name, description, important is not None, incident is not None]): raise ValueError(MESSAGES["AT_LEAST_ONE_REQUIRED"].format(", ".join(CASE_UPDATE_ARGS))) body: dict[str, Any] = {} mask_fields: list[str] = [] if display_name: body["displayName"] = display_name mask_fields.append("displayName") if description: body["description"] = description mask_fields.append("description") if important is not None: body["important"] = important mask_fields.append("important") if incident is not None: body["incident"] = incident mask_fields.append("incident") update_mask = ",".join(mask_fields) return body, update_mask def validate_case_tag_add_args(case_ids: list[str], tags: list[str]) -> tuple[list[str], list[str]]: """ Validate and clean case_ids and tags by removing null and empty string values. :type case_ids: list[str] :param case_ids: List of case IDs. :type tags: list[str] :param tags: List of tags. :return: Validated and cleaned case_ids and tags. :rtype: tuple[list[str], list[str]] """ valid_case_ids = validate_positive_integer_list(case_ids, arg_name="case_ids", display_name=CASE_ID_DISPLAY) valid_tags = strip_and_filter_list(tags) validate_argument(valid_tags, "tags") return valid_case_ids, valid_tags def validate_case_priority_change_args(case_ids: list[str], priority: str) -> tuple[list[str], str]: """ Validate and clean case_ids and priority. :type case_ids: list[str] :param case_ids: List of case IDs. :type priority: str :param priority: The new priority value. :return: Validated and cleaned case_ids and priority. :rtype: tuple[list[str], str] """ valid_case_ids = validate_positive_integer_list(case_ids, arg_name="case_ids", display_name=CASE_ID_DISPLAY) valid_priority = priority.strip().upper() if valid_priority not in VALID_CASE_PRIORITIES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("priority", ", ".join(VALID_CASE_PRIORITIES))) return valid_case_ids, valid_priority def validate_case_close_args(case_ids: list[str], close_reason: str) -> tuple[list[str], str]: """ Validate and clean case_ids and close_reason. :type case_ids: list[str] :param case_ids: List of case IDs. :type close_reason: str :param close_reason: The reason for closing the cases. :return: Validated and cleaned case_ids and close_reason. :rtype: tuple[list[str], str] """ valid_case_ids = validate_positive_integer_list(case_ids, arg_name="case_ids", display_name=CASE_ID_DISPLAY) valid_close_reason = close_reason.strip().upper() if valid_close_reason not in VALID_CASE_CLOSE_REASONS: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("close_reason", ", ".join(VALID_CASE_CLOSE_REASONS))) return valid_case_ids, valid_close_reason def validate_case_comment_list_args(case_id: str, page_size: int, sort_order: str) -> tuple[str, int, str]: """ Validate case_comment_list parameters. :type case_id: str :param case_id: The ID of the case. :type page_size: int :param page_size: Maximum number of comments to return. :type sort_order: str :param sort_order: Sort direction (Asc or Desc). :return: Validated case_id, page_size, and sort_order. :rtype: tuple[str, int, str] """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if page_size < 1 or page_size > MAX_PAGE_SIZE: raise ValueError(MESSAGES["INVALID_INT_RANGE"].format(page_size, "page_size", 1, MAX_PAGE_SIZE)) valid_sort_order = sort_order.strip().capitalize() if valid_sort_order not in VALID_SORT_ORDERS: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("sort_order", ", ".join(VALID_SORT_ORDERS))) return case_id, page_size, valid_sort_order def validate_case_fetch_filter_params(priorities: list, statuses: list, filter_logic: str) -> tuple: """ Validate and transform case filter parameters to API enum format. :type priorities: list :param priorities: List of case priority values to validate. :type statuses: list :param statuses: List of case status values to validate. :type filter_logic: str :param filter_logic: Logical operator used to join filter conditions — "AND" or "OR". :return: Tuple of (priorities, statuses, filter_logic) validated values. :rtype: tuple :raises ValueError: If any parameter value is not in the allowed values. """ validated_priorities = [] for priority in priorities: if priority.upper() not in VALID_CASE_PRIORITIES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("case_priorities", ", ".join(VALID_CASE_PRIORITIES))) validated_priorities.append(f"PRIORITY_{priority.upper()}") validated_statuses = [] for status in statuses: if status.upper() not in VALID_CASE_STATUSES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("case_statuses", ", ".join(VALID_CASE_STATUSES))) validated_statuses.append(status.upper()) if filter_logic not in VALID_CASE_FILTER_LOGIC: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("case_filter_logic", ", ".join(VALID_CASE_FILTER_LOGIC))) return validated_priorities, validated_statuses, filter_logic def validate_fetch_params(params: dict[str, Any], is_test: bool = False) -> dict[str, Any]: """ Validate and prepare the fetch incident parameters. :type params: dict[str, Any] :param params: Integration configuration parameters from demisto.params(). :type is_test: bool :param is_test: Whether this is being called from test-module. :return: Dictionary containing validated and ready-to-use fetch parameters. :rtype: dict[str, Any] """ first_fetch = params.get("first_fetch", DEFAULT_FIRST_FETCH).strip() first_fetch_dt = arg_to_datetime(first_fetch, arg_name="First Fetch Time") # type: ignore if first_fetch_dt.tzinfo is None: # type: ignore first_fetch_dt = first_fetch_dt.replace(tzinfo=UTC) # type: ignore now_utc = datetime.now(UTC) if first_fetch_dt > now_utc: # type: ignore raise ValueError(MESSAGES["FUTURE_DATE"]) max_first_fetch_dt = now_utc - timedelta(days=MAX_FIRST_FETCH_DAYS) first_fetch_clamped = False if any(boundary_str in first_fetch.lower() for boundary_str in MAX_FIRST_FETCH_BOUNDARY_STRINGS): first_fetch_dt += timedelta(minutes=1) # type: ignore if first_fetch_dt < max_first_fetch_dt: # type: ignore if is_test: raise ValueError(MESSAGES["INVALID_FIRST_FETCH"].format(first_fetch)) first_fetch_dt = max_first_fetch_dt first_fetch_clamped = True first_fetch_time = first_fetch_dt.strftime(DATE_FORMAT) # type: ignore max_fetch_ = params.get("max_fetch", DEFAULT_MAX_FETCH) if not check_valid_positive_number(str(max_fetch_)): raise ValueError(MESSAGES["INVALID_MAX_FETCH"].format(value=max_fetch_, max_limit=MAX_FETCH_LIMIT)) max_fetch_ = int(max_fetch_) if max_fetch_ > MAX_FETCH_LIMIT: if is_test: raise ValueError(MESSAGES["INVALID_MAX_FETCH"].format(value=max_fetch_, max_limit=MAX_FETCH_LIMIT)) else: demisto.debug( f"The Incidents fetch limit value is {max_fetch_}, which is greater than the maximum allowed value of " f"{MAX_FETCH_LIMIT}. Setting it to {MAX_FETCH_LIMIT}." ) max_fetch = min(MAX_FETCH_LIMIT, max_fetch_) raw_priorities = strip_and_filter_list(argToList(params.get("case_priorities", ""))) raw_statuses = strip_and_filter_list(argToList(params.get("case_statuses", ""))) environments = strip_and_filter_list(argToList(params.get("case_environments", ""))) tags = strip_and_filter_list(argToList(params.get("case_tags", ""))) filter_logic = params.get("case_filter_logic", DEFAULT_FILTER_LOGIC).strip().upper() priorities, statuses, filter_logic = validate_case_fetch_filter_params(raw_priorities, raw_statuses, filter_logic) return { "max_fetch": max_fetch, "first_fetch": first_fetch_time, "priorities": priorities, "statuses": statuses, "environments": environments, "tags": tags, "filter_logic": filter_logic, "first_fetch_clamped": first_fetch_clamped, } def validate_case_assign_args(client: Client, case_ids: list[str], assignee: str) -> tuple[list[str], str]: """ Validate and clean case_ids and resolve the assignee for the case assign command. SOC roles (prefixed with '@') are passed through unchanged. Email addresses are resolved to their SOAR user ID via the legacySoarUsers API. :type client: Client :param client: Client instance used to resolve email to user ID. :type case_ids: list[str] :param case_ids: List of case IDs. :type assignee: str :param assignee: The assignee email address or SOC role (e.g. @Tier1). :return: Validated case_ids and resolved assignee value. :rtype: tuple[list[str], str] """ valid_case_ids = validate_positive_integer_list(case_ids, arg_name="case_ids", display_name=CASE_ID_DISPLAY) resolved = resolve_assignees(client, [assignee], raise_on_not_found=True) return valid_case_ids, resolved[0] def validate_case_alert_list_args( case_id: str, page_size: int, sort_order: str, priorities: list[str], statuses: list[str], filter_logic: str = DEFAULT_FILTER_LOGIC, ) -> tuple[str, int, str, list[str], list[str], str]: """ Validate arguments for gcb-case-alert-list command. :type case_id: str :param case_id: The ID of the Case to list alerts for. :type page_size: int :param page_size: Maximum number of alerts to return. :type sort_order: str :param sort_order: Sort direction (Asc or Desc). :type priorities: list[str] :param priorities: List of priority filter values. :type statuses: list[str] :param statuses: List of status filter values. :type filter_logic: str :param filter_logic: Logical operator used to join filter conditions — "AND" or "OR". :raises ValueError: If any argument fails validation. :return: Validated (case_id, page_size, sort_order, priorities, statuses, filter_logic). :rtype: tuple """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, "Case ID")) if page_size < 1 or page_size > MAX_PAGE_SIZE: raise ValueError(MESSAGES["INVALID_INT_RANGE"].format(page_size, "page_size", 1, MAX_PAGE_SIZE)) valid_sort_order = sort_order.strip().title() if valid_sort_order not in VALID_SORT_ORDERS: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("sort_order", ", ".join(VALID_SORT_ORDERS))) validated_priorities = [] for priority in priorities: if priority.upper() not in VALID_CASE_ALERT_PRIORITIES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("priority", ", ".join(VALID_CASE_ALERT_PRIORITIES))) validated_priorities.append(priority.upper()) validated_statuses = [] for status in statuses: if status.upper() not in VALID_CASE_ALERT_STATUSES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("status", ", ".join(VALID_CASE_ALERT_STATUSES))) validated_statuses.append(status.upper()) valid_filter_logic = filter_logic.upper() if valid_filter_logic not in VALID_CASE_FILTER_LOGIC: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("filter_logic", ", ".join(VALID_CASE_FILTER_LOGIC))) return ( case_id, page_size, valid_sort_order, validated_priorities, validated_statuses, valid_filter_logic, ) def validate_case_alert_update_args( case_id: str, alert_id: str, status: str, priority: str, close_reason: str, close_comment: str, root_cause: str, ) -> tuple[dict[str, Any], str]: """ Validate arguments for gcb-case-alert-update and build the request body and updateMask. :type case_id: str :param case_id: Case ID the alert belongs to. :type alert_id: str :param alert_id: Alert ID to update. :type status: str :param status: New status for the alert (OPEN or CLOSE). :type priority: str :param priority: New priority for the alert. :type close_reason: str :param close_reason: Closure reason (required when status is CLOSE). :type close_comment: str :param close_comment: Comment to add when closing. :type root_cause: str :param root_cause: Root cause for the alert closure. :return: Tuple of (body dict, updateMask string). :rtype: tuple[dict[str, Any], str] """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) if not any([status, priority]): raise ValueError(MESSAGES["AT_LEAST_ONE_REQUIRED"].format(", ".join(CASE_ALERT_UPDATE_ARGS))) body: dict[str, Any] = {} mask_fields: list[str] = [] if status: valid_status = status.upper() if valid_status not in VALID_CASE_ALERT_STATUSES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("status", ", ".join(VALID_CASE_ALERT_STATUSES))) body["status"] = valid_status mask_fields.append("status") if valid_status == "CLOSE": if not close_reason: raise ValueError("close_reason is required when status is CLOSE.") if not root_cause: raise ValueError("root_cause is required when status is CLOSE.") valid_close_reason = close_reason.upper() if valid_close_reason not in VALID_CASE_ALERT_CLOSE_REASONS: raise ValueError( MESSAGES["VALIDATE_SINGLE_SELECT"].format("close_reason", ", ".join(VALID_CASE_ALERT_CLOSE_REASONS)) ) closure_details: dict[str, Any] = {"reason": valid_close_reason, "rootCause": root_cause} if close_comment: closure_details["comment"] = close_comment body["closureDetails"] = closure_details mask_fields.append("closureDetails") if priority: valid_priority = priority.upper() if valid_priority not in VALID_CASE_ALERT_PRIORITIES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("priority", ", ".join(VALID_CASE_ALERT_PRIORITIES))) body["priority"] = valid_priority mask_fields.append("priority") update_mask = ",".join(mask_fields) return body, update_mask def validate_case_alert_move_args(case_id: str, alert_id: str, destination_case_id: str) -> tuple[str, str, str]: """ Validate case_id, alert_id, and destination_case_id for the case alert move command. :type case_id: str :param case_id: The ID of the source Case. :type alert_id: str :param alert_id: The ID of the Case Alert to move. :type destination_case_id: str :param destination_case_id: The ID of the destination Case. :return: Validated case_id, alert_id, and destination_case_id. :rtype: tuple[str, str, str] """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) if not check_valid_positive_number(destination_case_id): raise ValueError( MESSAGES["INVALID_POSITIVE_INTEGER"].format("destination_case_id", destination_case_id, "Destination Case ID") ) return case_id, alert_id, destination_case_id def validate_case_alert_sla_set_args( case_id: str, alert_id: str, total_time: str, critical_time: str | None ) -> tuple[int, int | None]: """ Validate arguments for the case alert SLA set command. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type total_time: str :param total_time: The total SLA duration string. :type critical_time: str | None :param critical_time: The critical SLA threshold string. Optional. :return: total_time_ms, and critical_time_ms. :rtype: tuple[int, int | None] """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) total_time_ms = convert_time_to_ms(total_time, "total_time") critical_time_ms = convert_time_to_ms(critical_time, "critical_time") if critical_time else None if critical_time_ms is not None and total_time_ms <= critical_time_ms: raise ValueError("total_time must be greater than critical_time.") return total_time_ms, critical_time_ms def validate_case_alert_customfield_list_args(case_id: str, alert_id: str, page_size: int) -> tuple[str, str, int]: """ Validate arguments for the gcb-case-alert-customfield-list command. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type page_size: int :param page_size: Maximum number of custom field values to return. :return: Validated (case_id, alert_id, page_size). :rtype: tuple[str, str, int] """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) if page_size < 1 or page_size > MAX_PAGE_SIZE: raise ValueError(MESSAGES["INVALID_INT_RANGE"].format(page_size, "page_size", 1, MAX_PAGE_SIZE)) return case_id, alert_id, page_size def validate_case_alert_entity_list_args( case_id: str, alert_id: str, page_size: int, sort_order: str, filter_logic: str = DEFAULT_FILTER_LOGIC, ) -> tuple[str, str, int, str, str]: """ Validate arguments for the gcb-case-alert-entity-list command. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type page_size: int :param page_size: Maximum number of entities to return. :type sort_order: str :param sort_order: Sort direction (Asc or Desc). :type filter_logic: str :param filter_logic: Logical operator to combine filter conditions ('AND' or 'OR'). :return: Validated (case_id, alert_id, page_size, sort_order, filter_logic). :rtype: tuple[str, str, int, str, str] """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) if page_size < 1 or page_size > MAX_PAGE_SIZE: raise ValueError(MESSAGES["INVALID_INT_RANGE"].format(page_size, "page_size", 1, MAX_PAGE_SIZE)) valid_sort_order = sort_order.strip().capitalize() if valid_sort_order not in VALID_SORT_ORDERS: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("sort_order", ", ".join(VALID_SORT_ORDERS))) if filter_logic.upper() not in VALID_CASE_FILTER_LOGIC: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("filter_logic", ", ".join(VALID_CASE_FILTER_LOGIC))) return case_id, alert_id, page_size, valid_sort_order, filter_logic.upper() def validate_case_alert_entity_get_args(case_id: str, alert_id: str, entity_id: str) -> None: """ Validate arguments for the gcb-case-alert-entity-get command. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type entity_id: str :param entity_id: The ID of the Involved Entity. :rtype: None """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) if not check_valid_positive_number(entity_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", entity_id, ENTITY_ID_DISPLAY)) def validate_case_alert_entity_create_args(case_id: str, alert_id: str) -> None: """ Validate arguments for the gcb-case-alert-entity-create command. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :rtype: None """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) def validate_case_alert_entity_update_args(case_id: str, alert_id: str, entity_id: str) -> None: """ Validate arguments for the gcb-case-alert-entity-update command. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type entity_id: str :param entity_id: The ID of the Involved Entity. :rtype: None """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) if not check_valid_positive_number(entity_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", entity_id, ENTITY_ID_DISPLAY)) def validate_case_alert_entity_property_args(case_id: str, alert_id: str, entity_id: str) -> None: """ Validate arguments for the gcb-case-alert-entity-property-add and gcb-case-alert-entity-property-update commands. :type case_id: str :param case_id: The ID of the Case. :type alert_id: str :param alert_id: The ID of the Case Alert. :type entity_id: str :param entity_id: The ID of the Involved Entity. :rtype: None """ if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) if not check_valid_positive_number(entity_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", entity_id, ENTITY_ID_DISPLAY)) """ COMMAND FUNCTIONS """ def fetch_incidents( client: Client, params: dict[str, Any], last_run: dict[str, Any], is_test: bool = False ) -> tuple[list[dict[str, Any]], dict[str, Any]]: """ Fetch Google SecOps Cases and convert them into XSOAR incidents. :type client: Client :param client: Client object to interact with the Google SecOps API. :type params: dict[str, Any] :param params: Integration configuration parameters from demisto.params(). :type last_run: dict[str, Any] :param last_run: Last run state from demisto.getLastRun(). :type is_test: bool :param is_test: Indicates whether to test the connection to the Google SecOps server. :return: Tuple of (list of incidents, next last_run dict). :rtype: tuple[list[dict[str, Any]], dict[str, Any]] """ validated_params = validate_fetch_params(params, is_test=is_test) max_fetch = validated_params.get("max_fetch", DEFAULT_MAX_FETCH) first_fetch_time = validated_params.get("first_fetch") priorities = validated_params.get("priorities") statuses = validated_params.get("statuses") environments = validated_params.get("environments") tags = validated_params.get("tags") filter_logic: str = validated_params.get("filter_logic", DEFAULT_FILTER_LOGIC) first_fetch_clamped: bool = validated_params.get("first_fetch_clamped", False) cases_state = last_run.get("cases", {}) # Check if filter configuration changed from last fetch cycle; if yes, reset the page_token. current_filter_hash = compute_cases_filter_hash(priorities, statuses, environments, tags, filter_logic) stored_filter_hash = cases_state.get("filter_hash") filter_config_changed = stored_filter_hash is not None and stored_filter_hash != current_filter_hash raw_last_case_create_time = cases_state.get("last_case_create_time") stored_last_case_create_time = ( arg_to_datetime(raw_last_case_create_time).strftime(DATE_FORMAT) # type: ignore if raw_last_case_create_time else "" ) if filter_config_changed: demisto.debug("Fetch Cases: Filter config changed. Resetting page_token.") page_token = None if filter_config_changed else cases_state.get("page_token") ingested_case_ids: list = cases_state.get("ingested_case_ids", []) last_run_start_time = cases_state.get("start_time") start_dt = arg_to_datetime(last_run_start_time or first_fetch_time) start_time = start_dt.strftime(DATE_FORMAT) # type: ignore if not last_run_start_time: demisto.debug(f"Fetch Cases: No start_time in last run. Using first_fetch_time={first_fetch_time} as start_time.") if first_fetch_clamped: demisto.debug( f"Fetch Cases: First fetch value is older than {MAX_FIRST_FETCH_DAYS} days. " f"Setting it to {MAX_FIRST_FETCH_DAYS} days." ) demisto.debug(f"Fetch Cases: start_time={start_time}, page_token={page_token}, max_fetch={max_fetch}") case_filter = prepare_cases_filter( priorities=priorities, statuses=statuses, environments=environments, tags=tags, filter_logic=filter_logic, ) filter_start_time = stored_last_case_create_time if filter_config_changed and stored_last_case_create_time else start_time if filter_config_changed and stored_last_case_create_time: demisto.debug( f"Fetch Cases: Filter config changed. Using last_case_create_time={stored_last_case_create_time} as start_time." ) epoch_time = date_to_utc_epoch(filter_start_time, DATE_FORMAT) time_filter = f"createTime>={epoch_time}" case_filter = f"({case_filter}) AND {time_filter}" if case_filter else time_filter response = client.list_cases( page_size=max_fetch, case_filter=case_filter, page_token=page_token, order_by="createTime asc", ) cases = response.get("cases", []) next_page_token = response.get("nextPageToken") demisto.debug(f"Fetch Cases: Retrieved {len(cases)} cases. next_page_token present: {bool(next_page_token)}") if is_test: return [], {} incidents: list[dict[str, Any]] = [] ingested_cases, duplicate_cases = [], [] last_case_create_time: str = "" for case in cases: parts = case.get("name", "").split("cases/") case_id = parts[1] if len(parts) >= 2 else "" if not case_id: demisto.debug(f"Fetch Cases: Skipping case with missing or empty ID. Raw name: {case.get('name', '')}") continue if case_id in ingested_case_ids: duplicate_cases.append(case_id) continue case["caseId"] = case_id processed_incident: dict[str, Any] = case.copy() priority = case.get("priority", "PRIORITY_UNSPECIFIED") processed_incident["severity"] = CASE_SEVERITY_MAP.get(priority, 0) raw_create_time = processed_incident.get("createTime") create_time = arg_to_datetime(raw_create_time or "now").strftime(DATE_FORMAT) # type: ignore if raw_create_time: last_case_create_time = create_time incidents.append( { "name": f"Google SecOps Case: {case.get('displayName', case_id)}", "occurred": create_time, "rawJSON": json.dumps(remove_empty_elements_for_fetch(processed_incident)), "severity": processed_incident.get("severity"), "dbotMirrorId": f"case-{case_id}", } ) ingested_case_ids.append(case_id) ingested_cases.append(case_id) demisto.debug(f"Fetch Cases: Successfully fetched {len(ingested_cases)} cases.") multiline_logs_for_list(ingested_cases, "Ingested Cases: ") demisto.debug(f"Fetch Cases: Skipped {len(duplicate_cases)} duplicate cases.") multiline_logs_for_list(duplicate_cases, "Duplicate Cases: ") next_cases_state: dict[str, Any] = { "start_time": filter_start_time, "ingested_case_ids": ingested_case_ids, "filter_hash": current_filter_hash, "last_case_create_time": last_case_create_time or stored_last_case_create_time, } if next_page_token: next_cases_state["page_token"] = next_page_token demisto.debug("Fetch Cases: More pages available. Storing page_token for next cycle.") next_state: dict[str, Any] = last_run.copy() next_state["cases"] = next_cases_state demisto.debug( f"Fetch Cases: Checkpoint last run - start_time: {next_cases_state.get('start_time')}, " f"page_token: {next_cases_state.get('page_token')}, " f"last_case_create_time: {next_cases_state.get('last_case_create_time')}, " f"filter_hash: {next_cases_state.get('filter_hash')}" ) return incidents, next_state def test_module(client: Client, params: dict[str, Any]) -> str: """ Tests API connectivity and validity of provided parameters. Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful. Raises exceptions if something goes wrong. :type client: Client :param client: Google SecOps Case client to use. :type params: dict[str, Any] :param params: Integration configuration parameters from demisto.params(). :return: 'ok' if test passed, anything else will fail the test. :rtype: str """ if argToBoolean(params.get("isFetch", False)): fetch_incidents(client, params, last_run={}, is_test=True) else: client.list_cases(page_size=1) return "ok" def gcb_case_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Retrieve the list of Cases. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing optional filter and pagination parameters. Returns: CommandResults with readable output and context data. """ page_size: int = arg_to_number(args.get("page_size", DEFAULT_PAGE_SIZE), "page_size") # type: ignore page_token = args.get("page_token") sort_by = args.get("sort_by", DEFAULT_CASE_LIST_SORT_BY) sort_order = args.get("sort_order", DEFAULT_CASE_LIST_SORT_ORDER) advanced_filter = args.get("advanced_filter", "") priorities = strip_and_filter_list(argToList(args.get("priority", ""))) statuses = strip_and_filter_list(argToList(args.get("status", ""))) case_types = strip_and_filter_list(argToList(args.get("type", ""))) stages = strip_and_filter_list(argToList(args.get("stage", ""))) sources = strip_and_filter_list(argToList(args.get("source", ""))) raw_assignees = strip_and_filter_list(argToList(args.get("assignee", ""))) environments = strip_and_filter_list(argToList(args.get("environment", ""))) tags = strip_and_filter_list(argToList(args.get("tags", ""))) products = strip_and_filter_list(argToList(args.get("products", ""))) display_names = strip_and_filter_list(argToList(args.get("display_name", ""))) filter_logic = args.get("filter_logic", DEFAULT_FILTER_LOGIC) workflow_statuses = strip_and_filter_list(argToList(args.get("workflow_status", ""))) sla_statuses = strip_and_filter_list(argToList(args.get("sla", ""))) alerts_sla_statuses = strip_and_filter_list(argToList(args.get("alerts_sla", ""))) important: bool | None = arg_to_bool_or_none(args.get("important")) incident: bool | None = arg_to_bool_or_none(args.get("incident")) create_start_time_dt = arg_to_datetime(args.get("create_start_time"), arg_name="create_start_time") # type: ignore create_end_time_dt = arg_to_datetime(args.get("create_end_time"), arg_name="create_end_time") # type: ignore update_start_time_dt = arg_to_datetime(args.get("update_start_time"), arg_name="update_start_time") # type: ignore update_end_time_dt = arg_to_datetime(args.get("update_end_time"), arg_name="update_end_time") # type: ignore create_start_time, create_end_time, update_start_time, update_end_time = validate_case_list_date_args( create_start_time_dt, create_end_time_dt, update_start_time_dt, update_end_time_dt ) ( page_size, sort_by, sort_order, priorities, statuses, case_types, workflow_statuses, sla_statuses, alerts_sla_statuses, filter_logic, ) = validate_case_list_args( page_size, sort_by, sort_order, priorities, statuses, case_types, workflow_statuses, sla_statuses, alerts_sla_statuses, filter_logic, ) if advanced_filter: case_filter = advanced_filter else: assignees = resolve_assignees(client, raw_assignees) case_filter = prepare_cases_filter( display_names=display_names, priorities=priorities, statuses=statuses, case_types=case_types, stages=stages, sources=sources, assignees=assignees, environments=environments, tags=tags, products=products, important=important, incident=incident, workflow_statuses=workflow_statuses, sla_statuses=sla_statuses, alerts_sla_statuses=alerts_sla_statuses, filter_logic=filter_logic, ) date_filter = prepare_cases_date_filter( create_start_time=create_start_time, create_end_time=create_end_time, update_start_time=update_start_time, update_end_time=update_end_time, ) if date_filter: case_filter = f"({case_filter}) AND {date_filter}" if case_filter else date_filter order_by = f"{sort_by} {sort_order.lower()}" response = client.list_cases(page_size=page_size, case_filter=case_filter, page_token=page_token, order_by=order_by) cases = response.get("cases", []) if not cases: return CommandResults( readable_output=MESSAGES["NO_RECORDS_FOUND"].format("cases"), raw_response=response, ) outputs, readable_output = prepare_context_hr_gcb_case_list(response) return CommandResults( outputs=remove_empty_elements(outputs), readable_output=readable_output, raw_response=response, ) def gcb_case_get_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Retrieve a specific Case by its ID. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) response = client.get_case(case_id) if not response: return CommandResults(readable_output=MESSAGES["NO_RECORDS_FOUND"].format("case information")) context, readable_output = prepare_context_hr_gcb_case_get_update(response) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=context, readable_output=readable_output, raw_response=response, ) def gcb_case_update_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Update the properties of a Case. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id and optional update fields. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") display_name = args.get("display_name", "") description = args.get("description", "") important = arg_to_bool_or_none(args.get("important")) incident = arg_to_bool_or_none(args.get("incident")) body, update_mask = validate_case_update_args(case_id, display_name, description, important, incident) response = client.update_case(case_id, body, update_mask) context, readable_output = prepare_context_hr_gcb_case_get_update(response, title="Updated Case Information", case_id=case_id) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=context, readable_output=readable_output, raw_response=response, ) def gcb_case_tag_add_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Add the specified tags to the cases in bulk. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_ids and tags. Returns: CommandResults with readable output and context data. """ case_ids = argToList(validate_argument(args.get("case_ids"), "case_ids")) tags = argToList(validate_argument(args.get("tags"), "tags")) case_ids, tags = validate_case_tag_add_args(case_ids, tags) client.case_tag_add(case_ids, tags) outputs = [{"caseId": case_id, "recentlyAddedTags": tags} for case_id in case_ids] readable_output = f"Tags {', '.join(tags)} successfully added to cases {', '.join(case_ids)}." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_tag_remove_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Remove the specified tag from a Case. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id and tag. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") tag = validate_argument(args.get("tag"), "tag") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) client.case_tag_remove(case_id, tag) outputs = {"caseId": case_id, "recentlyRemovedTag": tag} readable_output = f"Tag {tag} successfully removed from case {case_id}." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_priority_change_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Change the priority of the specified cases in bulk. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_ids and priority. Returns: CommandResults with readable output and context data. """ case_ids = argToList(validate_argument(args.get("case_ids"), "case_ids")) priority = validate_argument(args.get("priority"), "priority") case_ids, priority = validate_case_priority_change_args(case_ids, priority) client.case_priority_change(case_ids, priority) outputs = [{"caseId": case_id, "priority": priority} for case_id in case_ids] readable_output = f"Priority of cases {', '.join(case_ids)} successfully changed to {priority}." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_stage_definition_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """ List case stage definitions configured in the instance. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments (unused). Returns: CommandResults with readable output and context data. """ response = client.case_stage_definitions_list() case_stage_definitions = response.get("caseStageDefinitions", []) display_names = [s.get("displayName") for s in case_stage_definitions if s.get("displayName")] readable_output = f"Case Stage Definitions: {', '.join(display_names)}" return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseStageDefinition"], outputs=display_names, readable_output=readable_output, raw_response=response, ) def gcb_case_stage_change_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Change the workflow stage of the specified cases in bulk. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_ids and stage. Returns: CommandResults with readable output and context data. """ case_ids = argToList(validate_argument(args.get("case_ids"), "case_ids")) stage = validate_argument(args.get("stage"), "stage") case_ids = validate_positive_integer_list(case_ids, arg_name="case_ids", display_name=CASE_ID_DISPLAY) client.case_stage_change(case_ids, stage) outputs = [{"caseId": case_id, "stage": stage} for case_id in case_ids] readable_output = f"Stage of cases {', '.join(case_ids)} successfully changed to {stage}." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_reopen_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Reopen the specified cases in bulk. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_ids and reopen_comment. Returns: CommandResults with readable output and context data. """ case_ids = argToList(validate_argument(args.get("case_ids"), "case_ids")) reopen_comment = validate_argument(args.get("reopen_comment"), "reopen_comment") case_ids = validate_positive_integer_list(case_ids, arg_name="case_ids", display_name=CASE_ID_DISPLAY) client.case_reopen(case_ids, reopen_comment) outputs = [{"caseId": case_id, "status": "OPENED"} for case_id in case_ids] readable_output = f"Cases {', '.join(case_ids)} successfully reopened." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_close_definition_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """ List case close definitions configured in the instance. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments (unused). Returns: CommandResults with readable output and context data. """ response = client.case_close_definitions_list() case_close_definitions = response.get("caseCloseDefinitions", []) if not case_close_definitions: readable_output = MESSAGES["NO_RECORDS_FOUND"].format("case close definitions") return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseCloseDefinition"], outputs=[], readable_output=readable_output, raw_response=response, ) table_data = [] for definition in case_close_definitions: table_data.append( { "Close Reason": definition.get("closeReason", ""), "Root Cause": definition.get("rootCause", ""), } ) readable_output = tableToMarkdown( "Case Close Definitions", table_data, headers=["Close Reason", "Root Cause"], removeNull=True ) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseCloseDefinition"], outputs=case_close_definitions, readable_output=readable_output, raw_response=response, ) def gcb_case_close_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Close the specified cases in bulk. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_ids, close_reason, root_cause, and optional close_comment. Returns: CommandResults with readable output and context data. """ case_ids = argToList(validate_argument(args.get("case_ids"), "case_ids")) close_reason = validate_argument(args.get("close_reason"), "close_reason") root_cause = validate_argument(args.get("root_cause"), "root_cause") close_comment = args.get("close_comment") case_ids, close_reason = validate_case_close_args(case_ids, close_reason) client.case_close(case_ids, close_reason, root_cause, close_comment) outputs = [ { "caseId": case_id, "status": "CLOSED", "closureDetails": { "reason": close_reason, "rootCause": root_cause, "comment": close_comment if close_comment else None, }, } for case_id in case_ids ] outputs = remove_empty_elements(outputs) readable_output = f"Cases {', '.join(case_ids)} successfully closed with reason {close_reason}." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_assign_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Assign the specified cases to an analyst or SOC role. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_ids and assignee. Returns: CommandResults with readable output and context data. """ case_ids = argToList(validate_argument(args.get("case_ids"), "case_ids")) assignee = validate_argument(args.get("assignee"), "assignee") case_ids, resolved_user = validate_case_assign_args(client, case_ids, assignee) client.case_assign(case_ids, resolved_user) outputs = [{"caseId": case_id, "assignee": resolved_user} for case_id in case_ids] readable_output = f"Cases {', '.join(case_ids)} successfully assigned to {assignee}." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_comment_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Retrieve the list of comments for a specified case. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id and optional pagination/sorting parameters. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") page_size = arg_to_number(args.get("page_size", DEFAULT_PAGE_SIZE), "page_size") or DEFAULT_PAGE_SIZE page_token = args.get("page_token") sort_by = args.get("sort_by", DEFAULT_COMMENT_SORT_BY) sort_order = args.get("sort_order", DEFAULT_COMMENT_SORT_ORDER) case_id, page_size, sort_order = validate_case_comment_list_args(case_id, page_size, sort_order) response = client.case_comments_list(case_id, page_size, page_token, sort_by, sort_order) case_comments = response.get("caseComments", []) if not case_comments: readable_output = MESSAGES["NO_RECORDS_FOUND"].format("case comments") return CommandResults( readable_output=readable_output, raw_response=response, ) outputs, readable_output = prepare_context_hr_case_comment_list(response) return CommandResults( outputs=outputs, readable_output=readable_output, raw_response=response, ) def gcb_case_comment_create_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Add a comment to the specified case. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id and comment. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) comment = validate_argument(args.get("comment"), "comment") response = client.case_comment_create(case_id, comment) outputs, readable_output = prepare_context_hr_case_comment_create(response) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseComment"], outputs_key_field="name", outputs=outputs, readable_output=readable_output, raw_response=response, ) def gcb_case_sla_pause_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Pause the SLA timer for the specified case. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id and optional message. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) message = args.get("message") client.case_sla_pause(case_id, message) outputs = {"caseId": case_id, "slaStatus": "PAUSED"} readable_output = f"SLA timer for case {case_id} successfully paused." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_sla_resume_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Resume the SLA timer for the specified case. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) client.case_sla_resume(case_id) outputs = {"caseId": case_id, "slaStatus": "SLA_EXPIRATION_STATUS_UNSPECIFIED"} readable_output = f"SLA timer for case {case_id} successfully resumed." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Case"], outputs_key_field="caseId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_alert_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Retrieve the list of Case Alerts associated with a specific Case. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id and optional filter/pagination parameters. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") page_size: int = arg_to_number(args.get("page_size", DEFAULT_PAGE_SIZE), "page_size") # type: ignore page_token = args.get("page_token") sort_by = args.get("sort_by", DEFAULT_CASE_ALERT_LIST_SORT_BY) sort_order = args.get("sort_order", DEFAULT_CASE_ALERT_LIST_SORT_ORDER) advanced_filter = args.get("advanced_filter", "") display_names = strip_and_filter_list(argToList(args.get("display_name", ""))) priorities = strip_and_filter_list(argToList(args.get("priority", ""))) statuses = strip_and_filter_list(argToList(args.get("status", ""))) products = strip_and_filter_list(argToList(args.get("product", ""))) vendors = strip_and_filter_list(argToList(args.get("vendor", ""))) environments = strip_and_filter_list(argToList(args.get("environment", ""))) source_system_names = strip_and_filter_list(argToList(args.get("source_system_name", ""))) tags = strip_and_filter_list(argToList(args.get("tag", ""))) manual: bool | None = arg_to_bool_or_none(args.get("manual")) filter_logic = args.get("filter_logic", DEFAULT_FILTER_LOGIC) create_start_time_dt = arg_to_datetime(args.get("create_start_time"), arg_name="create_start_time") # type: ignore create_end_time_dt = arg_to_datetime(args.get("create_end_time"), arg_name="create_end_time") # type: ignore update_start_time_dt = arg_to_datetime(args.get("update_start_time"), arg_name="update_start_time") # type: ignore update_end_time_dt = arg_to_datetime(args.get("update_end_time"), arg_name="update_end_time") # type: ignore create_start_time, create_end_time, update_start_time, update_end_time = validate_case_alert_list_date_args( create_start_time_dt, create_end_time_dt, update_start_time_dt, update_end_time_dt ) case_id, page_size, sort_order, priorities, statuses, filter_logic = validate_case_alert_list_args( case_id, page_size, sort_order, priorities, statuses, filter_logic ) if advanced_filter: alert_filter = advanced_filter else: alert_filter = prepare_case_alerts_filter( display_names=display_names, priorities=priorities, statuses=statuses, products=products, vendors=vendors, environments=environments, source_system_names=source_system_names, tags=tags, manual=manual, filter_logic=filter_logic, ) date_filter = prepare_case_alerts_date_filter( create_start_time=create_start_time, create_end_time=create_end_time, update_start_time=update_start_time, update_end_time=update_end_time, ) if date_filter: alert_filter = f"({alert_filter}) AND {date_filter}" if alert_filter else date_filter order_by = f"{sort_by} {sort_order}" response = client.list_case_alerts( case_id=case_id, page_size=page_size, page_token=page_token, alert_filter=alert_filter, order_by=order_by, ) case_alerts = response.get("caseAlerts", []) if not case_alerts: return CommandResults( readable_output=MESSAGES["NO_RECORDS_FOUND"].format("case alerts"), raw_response=response, ) outputs, readable_output = prepare_context_hr_gcb_case_alert_list(response) return CommandResults( outputs=remove_empty_elements(outputs), readable_output=readable_output, raw_response=response, ) def gcb_case_alert_get_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Retrieve detailed information about a specific Case Alert by its ID. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id and alert_id. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) response = client.get_case_alert(case_id, alert_id) if not response: return CommandResults(readable_output=MESSAGES["NO_RECORDS_FOUND"].format("case alert information")) context, readable_output = prepare_context_hr_gcb_case_alert_get_update(response) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseAlert"], outputs_key_field="alertId", outputs=context, readable_output=readable_output, raw_response=response, ) def gcb_case_alert_update_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Update the properties of an existing Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id and optional update fields. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") status = args.get("status", "") priority = args.get("priority", "") close_reason = args.get("close_reason", "") close_comment = args.get("close_comment", "") root_cause = args.get("root_cause", "") body, update_mask = validate_case_alert_update_args( case_id, alert_id, status, priority, close_reason, close_comment, root_cause ) response = client.update_case_alert(case_id, alert_id, body, update_mask) context, readable_output = prepare_context_hr_gcb_case_alert_get_update( response, title="Updated Case Alert Information", alert_id=alert_id ) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseAlert"], outputs_key_field="alertId", outputs=context, readable_output=readable_output, raw_response=response, ) def gcb_case_alert_tag_add_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Add a tag to a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, and tag. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") tag = validate_argument(args.get("tag"), "tag") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) client.case_alert_tag_add(case_id, alert_id, tag) outputs = {"alertId": alert_id, "caseId": int(case_id), "recentlyAddedTag": tag} readable_output = f"Tag {tag} successfully added to alert {alert_id}." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseAlert"], outputs_key_field="alertId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_alert_tag_remove_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Remove a tag from a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, and tag. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") tag = validate_argument(args.get("tag"), "tag") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) client.case_alert_tag_remove(case_id, alert_id, tag) outputs = {"alertId": alert_id, "caseId": int(case_id), "recentlyRemovedTag": tag} readable_output = f"Tag {tag} successfully removed from alert {alert_id}." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseAlert"], outputs_key_field="alertId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_alert_move_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Move a Case Alert to a different Case. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, and destination_case_id. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") destination_case_id = validate_argument(args.get("destination_case_id"), "destination_case_id") case_id, alert_id, destination_case_id = validate_case_alert_move_args(case_id, alert_id, destination_case_id) response = client.case_alert_move(case_id, alert_id, destination_case_id) errors = response.get("errors", []) if errors: raise DemistoException("\n".join(errors)) new_case_id = response.get("newCaseId") outputs = { "alertId": alert_id, "caseId": int(new_case_id), # type: ignore } readable_output = f"Successfully moved Alert `{alert_id}` to Case `{new_case_id}`." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseAlert"], outputs_key_field="alertId", outputs=outputs, readable_output=readable_output, raw_response=response, ) def gcb_case_alert_sla_pause_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Pause the SLA timer for the specified Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, and optional message. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) message = args.get("message") client.case_alert_sla_pause(case_id, alert_id, message) outputs = {"alertId": alert_id, "caseId": int(case_id), "slaExpirationStatus": "PAUSED"} readable_output = f"SLA timer for alert {alert_id} successfully paused." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseAlert"], outputs_key_field="alertId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_alert_sla_resume_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Resume the SLA timer for the specified Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id and alert_id. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) client.case_alert_sla_resume(case_id, alert_id) outputs = {"alertId": alert_id, "caseId": int(case_id), "slaExpirationStatus": "SLA_EXPIRATION_STATUS_UNSPECIFIED"} readable_output = f"SLA timer for alert {alert_id} successfully resumed." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseAlert"], outputs_key_field="alertId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_alert_sla_set_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Set the SLA parameters for the specified Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, total_time, and optional critical_time. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") total_time = validate_argument(args.get("total_time"), "total_time") critical_time = args.get("critical_time") total_time_ms, critical_time_ms = validate_case_alert_sla_set_args(case_id, alert_id, total_time, critical_time) client.case_alert_sla_set(case_id, alert_id, total_time_ms, critical_time_ms) now_ms = int(datetime.now(UTC).timestamp() * 1000) outputs: dict[str, Any] = { "alertId": alert_id, "caseId": int(case_id), "slaExpirationTime": str(now_ms + total_time_ms), } if critical_time_ms is not None: outputs["slaCriticalExpirationTime"] = str(now_ms + critical_time_ms) readable_output = f"SLA for Alert `{alert_id}` successfully set." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["CaseAlert"], outputs_key_field="alertId", outputs=outputs, readable_output=readable_output, raw_response=outputs, ) def gcb_case_alert_recommendation_create_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Initiate an asynchronous AI recommendation for a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id and alert_id. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) if not check_valid_positive_number(alert_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", alert_id, ALERT_ID_DISPLAY)) response = client.case_alert_recommendation_create(case_id, alert_id) recommendation_id = response.get("recommendationId", "") readable_output = ( f"Successfully created the recommendation for the alert {alert_id}.\n\nRecommendation ID: {recommendation_id}" ) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["AlertRecommendation"], outputs_key_field="recommendationId", outputs=remove_empty_elements(response), readable_output=readable_output, raw_response=response, ) def gcb_case_alert_recommendation_fetch_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Fetch a previously generated AI recommendation for a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id and recommendation_id. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") recommendation_id = validate_argument(args.get("recommendation_id"), "recommendation_id") if not check_valid_positive_number(case_id): err_msg = MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY) raise ValueError(err_msg) response = client.case_alert_fetch_recommendation(case_id, recommendation_id) if not response: return CommandResults(readable_output=MESSAGES["NO_RECORDS_FOUND"].format("alert recommendation")) context, readable_output = prepare_context_hr_gcb_case_alert_recommendation_fetch(response, recommendation_id) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["AlertRecommendation"], outputs_key_field="recommendationId", outputs=context, readable_output=readable_output, raw_response=response, ) def gcb_case_alert_customfield_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Retrieve the list of custom field values associated with a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, and optional pagination parameters. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") page_size: int = arg_to_number(args.get("page_size", DEFAULT_PAGE_SIZE), "page_size") # type: ignore page_token = args.get("page_token") case_id, alert_id, page_size = validate_case_alert_customfield_list_args(case_id, alert_id, page_size) response = client.case_alert_customfield_list( case_id=case_id, alert_id=alert_id, page_size=page_size, page_token=page_token, order_by="customFieldId asc", ) custom_field_values = response.get("customFieldValues", []) if not custom_field_values: return CommandResults( readable_output=MESSAGES["NO_RECORDS_FOUND"].format("custom field values"), raw_response=response, ) field_ids = list({cfv.get("customFieldId") for cfv in custom_field_values if cfv.get("customFieldId")}) custom_field_id_to_display_name: dict[str, str] = {} if field_ids: filter_expr = " OR ".join(f"id = {fid}" for fid in field_ids) custom_fields_response = client.list_custom_fields( page_size=len(field_ids), custom_field_filter=f"({filter_expr})", ) for field in custom_fields_response.get("customFields", []): fid = field.get("id", "") if fid: custom_field_id_to_display_name[fid] = field.get("displayName", "") outputs, readable_output = prepare_context_hr_gcb_case_alert_customfield_list(response, custom_field_id_to_display_name) return CommandResults( outputs=outputs, readable_output=readable_output, raw_response=response, ) def gcb_case_alert_entity_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Retrieve the list of Entities associated with a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, and optional filter/pagination parameters. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") page_size = arg_to_number(args.get("page_size", DEFAULT_PAGE_SIZE), "page_size") or DEFAULT_PAGE_SIZE page_token = args.get("page_token") sort_by = args.get("sort_by", DEFAULT_ALERT_ENTITY_SORT_BY) sort_order = args.get("sort_order", DEFAULT_ALERT_ENTITY_SORT_ORDER) filter_logic = args.get("filter_logic", DEFAULT_FILTER_LOGIC) advanced_filter = args.get("advanced_filter", "") entity_types = strip_and_filter_list(argToList(args.get("entity_type", ""))) threat_sources = strip_and_filter_list(argToList(args.get("threat_source", ""))) operating_systems = strip_and_filter_list(argToList(args.get("operating_system", ""))) network_titles = strip_and_filter_list(argToList(args.get("network_title", ""))) network_priorities = strip_and_filter_list(argToList(args.get("network_priority", ""))) environments = strip_and_filter_list(argToList(args.get("environment", ""))) suspicious: bool | None = arg_to_bool_or_none(args.get("suspicious")) internal: bool | None = arg_to_bool_or_none(args.get("internal")) attacker: bool | None = arg_to_bool_or_none(args.get("attacker")) pivot: bool | None = arg_to_bool_or_none(args.get("pivot")) enriched: bool | None = arg_to_bool_or_none(args.get("enriched")) artifact: bool | None = arg_to_bool_or_none(args.get("artifact")) vulnerable: bool | None = arg_to_bool_or_none(args.get("vulnerable")) manually_created: bool | None = arg_to_bool_or_none(args.get("manually_created")) case_id, alert_id, page_size, sort_order, filter_logic = validate_case_alert_entity_list_args( case_id, alert_id, page_size, sort_order, filter_logic ) if advanced_filter: entity_filter = advanced_filter else: entity_filter = prepare_alert_entity_filter( entity_types=entity_types, suspicious=suspicious, internal=internal, attacker=attacker, pivot=pivot, enriched=enriched, artifact=artifact, vulnerable=vulnerable, manually_created=manually_created, threat_sources=threat_sources, operating_systems=operating_systems, network_titles=network_titles, network_priorities=network_priorities, environments=environments, filter_logic=filter_logic, ) order_by = f"{sort_by} {sort_order.lower()}" response = client.case_alert_involved_entities_list( case_id=case_id, alert_id=alert_id, page_size=page_size, page_token=page_token, entity_filter=entity_filter or None, order_by=order_by, ) entities = response.get("involvedEntities", []) if not entities: return CommandResults( readable_output=MESSAGES["NO_RECORDS_FOUND"].format("alert entities"), raw_response=response, ) outputs, readable_output = prepare_context_hr_case_alert_entity_list(response) return CommandResults( outputs=outputs, readable_output=readable_output, raw_response=response, ) def gcb_case_alert_entity_get_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Retrieve detailed information about a specific Involved Entity in a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, and entity_id. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") entity_id = validate_argument(args.get("entity_id"), "entity_id") validate_case_alert_entity_get_args(case_id, alert_id, entity_id) response = client.case_alert_involved_entity_get(case_id=case_id, alert_id=alert_id, entity_id=entity_id) if not response: return CommandResults( readable_output=MESSAGES["NO_RECORDS_FOUND"].format("entity information"), raw_response=response, ) outputs, readable_output = prepare_context_hr_case_alert_entity_get(response, alert_id=alert_id) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["AlertEntity"], outputs_key_field="id", outputs=outputs, readable_output=readable_output, raw_response=response, ) def gcb_case_alert_entity_create_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Manually create a new Involved Entity within a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, entity_type, and optional fields. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") identifier = validate_argument(args.get("identifier"), "identifier") entity_type = validate_argument(args.get("entity_type"), "entity_type") suspicious = arg_to_bool_or_none(args.get("suspicious", False)) internal = arg_to_bool_or_none(args.get("internal", False)) attacker = arg_to_bool_or_none(args.get("attacker")) pivot = arg_to_bool_or_none(args.get("pivot")) operating_system = args.get("operating_system") or None network_title = args.get("network_title") or None threat_source = args.get("threat_source") or None validate_case_alert_entity_create_args(case_id, alert_id) network_priority = arg_to_number(args.get("network_priority"), "network_priority") if network_priority is not None and network_priority < 0: raise ValueError(MESSAGES["INVALID_NON_NEGATIVE_INTEGER"].format(network_priority, "network_priority")) response = client.case_alert_involved_entity_create( case_id=case_id, alert_id=alert_id, identifier=identifier, entity_type=entity_type, suspicious=suspicious, internal=internal, attacker=attacker, pivot=pivot, operating_system=operating_system, network_title=network_title, threat_source=threat_source, network_priority=network_priority, ) if not response: return CommandResults( readable_output=MESSAGES["NO_RECORDS_FOUND"].format("entity information"), raw_response=response, ) outputs, readable_output = prepare_context_hr_case_alert_entity_get(response, alert_id=alert_id) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["AlertEntity"], outputs_key_field="id", outputs=outputs, readable_output=readable_output, raw_response=response, ) def gcb_case_alert_entity_update_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Update an existing Involved Entity within a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, entity_id, and optional fields to update. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") entity_id = validate_argument(args.get("entity_id"), "entity_id") suspicious = arg_to_bool_or_none(args.get("suspicious")) internal = arg_to_bool_or_none(args.get("internal")) attacker = arg_to_bool_or_none(args.get("attacker")) pivot = arg_to_bool_or_none(args.get("pivot")) operating_system = args.get("operating_system") network_title = args.get("network_title") threat_source = args.get("threat_source") network_priority_str = args.get("network_priority") if not any( [ suspicious is not None, internal is not None, attacker is not None, pivot is not None, operating_system is not None, network_title is not None, threat_source is not None, network_priority_str is not None, ] ): raise ValueError(MESSAGES["AT_LEAST_ONE_REQUIRED"].format(", ".join(CASE_ALERT_ENTITY_UPDATE_ARGS))) validate_case_alert_entity_update_args(case_id, alert_id, entity_id) network_priority = arg_to_number(network_priority_str, "network_priority") if network_priority is not None and network_priority < 0: raise ValueError(MESSAGES["INVALID_NON_NEGATIVE_INTEGER"].format(network_priority, "network_priority")) response = client.case_alert_involved_entity_update( case_id=case_id, alert_id=alert_id, entity_id=entity_id, suspicious=suspicious, internal=internal, attacker=attacker, pivot=pivot, operating_system=operating_system, network_title=network_title, threat_source=threat_source, network_priority=network_priority, ) if not response: return CommandResults( readable_output=MESSAGES["NO_RECORDS_FOUND"].format("entity information"), raw_response=response, ) outputs, readable_output = prepare_context_hr_case_alert_entity_get(response, "Updated Entity Information", alert_id=alert_id) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["AlertEntity"], outputs_key_field="id", outputs=outputs, readable_output=readable_output, raw_response=response, ) def _gcb_case_alert_entity_property_command(client: Client, args: dict[str, Any], action: str) -> CommandResults: """ Add/Update a custom property to an Involved Entity in a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, entity_id, key, and value. action: Verb prefix for the HR string — "Added" or "Updated". Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_id = validate_argument(args.get("alert_id"), "alert_id") entity_id = validate_argument(args.get("entity_id"), "entity_id") key = validate_argument(args.get("key"), "key") value = validate_argument(args.get("value"), "value") validate_case_alert_entity_property_args(case_id, alert_id, entity_id) if action == "Added": client_method = client.case_alert_involved_entity_add_property else: client_method = client.case_alert_involved_entity_update_property response = client_method(case_id=case_id, alert_id=alert_id, entity_id=entity_id, key=key, value=value) if not response: return CommandResults( readable_output=MESSAGES["NO_RECORDS_FOUND"].format("entity information"), raw_response=response, ) outputs, _ = prepare_context_hr_case_alert_entity_get(response, alert_id=alert_id) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["AlertEntity"], outputs_key_field="id", outputs=outputs, readable_output=f"{action} Entity Property with key `{key}` and value `{value}`.\n", raw_response=response, ) def gcb_case_alert_entity_property_add_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Add a new custom property to an Involved Entity in a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, entity_id, key, and value. Returns: CommandResults with readable output and context data. """ return _gcb_case_alert_entity_property_command(client, args, action="Added") def gcb_case_alert_entity_property_update_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Update an existing custom property value on an Involved Entity in a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing case_id, alert_id, entity_id, key, and value. Returns: CommandResults with readable output and context data. """ return _gcb_case_alert_entity_property_command(client, args, action="Updated") def gcb_playbook_list_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Retrieve the list of enabled playbooks. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments containing optional environment and execution_scope parameters. Returns: CommandResults with readable output and context data. """ case_environment = args.get("environment") execution_scope = args.get("execution_scope", DEFAULT_EXECUTION_SCOPE).upper() if execution_scope not in VALID_EXECUTION_SCOPES: raise ValueError(MESSAGES["VALIDATE_SINGLE_SELECT"].format("execution_scope", ", ".join(VALID_EXECUTION_SCOPES))) response = client.list_enabled_playbooks(case_environment=case_environment, execution_scope=execution_scope) playbooks = response.get("payload", []) if not playbooks: return CommandResults( readable_output=MESSAGES["NO_RECORDS_FOUND"].format("enabled playbooks"), raw_response=response, ) outputs, readable_output = prepare_context_hr_gcb_playbook_list(response) return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["Playbook"], outputs_key_field="workflowDefinitionIdentifier", outputs=outputs, readable_output=readable_output, raw_response=response, ) def gcb_playbook_attach_command(client: Client, args: dict[str, Any]) -> CommandResults: """ Manually attach (trigger) a specific playbook to a Case Alert. Args: client: Client object used to interact with the Google SecOps API. args: Command arguments. Returns: CommandResults with readable output and context data. """ case_id = validate_argument(args.get("case_id"), "case_id") alert_group_identifier = validate_argument(args.get("alert_group_identifier"), "alert_group_identifier") alert_identifier = validate_argument(args.get("alert_identifier"), "alert_identifier") playbook_name = validate_argument(args.get("playbook_name"), "playbook_name") original_workflow_definition_identifier = args.get("original_workflow_definition_identifier") if not check_valid_positive_number(case_id): raise ValueError(MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", case_id, CASE_ID_DISPLAY)) response = client.playbook_attach( case_id=case_id, alert_group_identifier=alert_group_identifier, alert_identifier=alert_identifier, playbook_name=playbook_name, original_workflow_definition_identifier=original_workflow_definition_identifier, ) success = response.get("payload", False) outputs = { "caseId": case_id, "alertGroupIdentifier": alert_group_identifier, "alertIdentifier": alert_identifier, "playbookName": playbook_name, "originalWorkflowDefinitionIdentifier": original_workflow_definition_identifier, "success": success, } if success: readable_output = f"Playbook '{playbook_name}' successfully attached to alert {alert_identifier} in case {case_id}." else: readable_output = f"Failed to attach playbook '{playbook_name}' to alert {alert_identifier} in case {case_id}." return CommandResults( outputs_prefix=SECOPS_OUTPUT_PATHS["PlaybookAttach"], outputs_key_field="alertIdentifier", outputs=remove_empty_elements(outputs), readable_output=readable_output, raw_response=response, ) def main(): """PARSE AND VALIDATE INTEGRATION PARAMS.""" params = demisto.params() remove_nulls_from_dictionary(trim_spaces_from_args(params)) proxy = argToBoolean(params.get("proxy", False)) disable_ssl = argToBoolean(params.get("insecure", False)) args = demisto.args() command = demisto.command() secops_commands = { "gcb-case-list": gcb_case_list_command, "gcb-case-get": gcb_case_get_command, "gcb-case-update": gcb_case_update_command, "gcb-case-tag-add": gcb_case_tag_add_command, "gcb-case-tag-remove": gcb_case_tag_remove_command, "gcb-case-priority-change": gcb_case_priority_change_command, "gcb-case-stage-definition-list": gcb_case_stage_definition_list_command, "gcb-case-stage-change": gcb_case_stage_change_command, "gcb-case-reopen": gcb_case_reopen_command, "gcb-case-close": gcb_case_close_command, "gcb-case-close-definition-list": gcb_case_close_definition_list_command, "gcb-case-assign": gcb_case_assign_command, "gcb-case-comment-list": gcb_case_comment_list_command, "gcb-case-comment-create": gcb_case_comment_create_command, "gcb-case-sla-pause": gcb_case_sla_pause_command, "gcb-case-sla-resume": gcb_case_sla_resume_command, "gcb-case-alert-list": gcb_case_alert_list_command, "gcb-case-alert-get": gcb_case_alert_get_command, "gcb-case-alert-update": gcb_case_alert_update_command, "gcb-case-alert-tag-add": gcb_case_alert_tag_add_command, "gcb-case-alert-tag-remove": gcb_case_alert_tag_remove_command, "gcb-case-alert-move": gcb_case_alert_move_command, "gcb-case-alert-sla-pause": gcb_case_alert_sla_pause_command, "gcb-case-alert-sla-resume": gcb_case_alert_sla_resume_command, "gcb-case-alert-sla-set": gcb_case_alert_sla_set_command, "gcb-case-alert-recommendation-create": gcb_case_alert_recommendation_create_command, "gcb-case-alert-recommendation-fetch": gcb_case_alert_recommendation_fetch_command, "gcb-case-alert-customfield-list": gcb_case_alert_customfield_list_command, "gcb-case-alert-entity-list": gcb_case_alert_entity_list_command, "gcb-case-alert-entity-get": gcb_case_alert_entity_get_command, "gcb-case-alert-entity-create": gcb_case_alert_entity_create_command, "gcb-case-alert-entity-update": gcb_case_alert_entity_update_command, "gcb-case-alert-entity-property-add": gcb_case_alert_entity_property_add_command, "gcb-case-alert-entity-property-update": gcb_case_alert_entity_property_update_command, "gcb-playbook-list": gcb_playbook_list_command, "gcb-playbook-attach": gcb_playbook_attach_command, } demisto.debug(f"Command being called is {command}") try: validate_configuration_parameters(params) # Initializing client Object client = Client(params=params, proxy=proxy, disable_ssl=disable_ssl) if command == "test-module": return_results(test_module(client, params)) elif command == "fetch-incidents": last_run = demisto.getLastRun() incidents, next_run = fetch_incidents(client, params, last_run) demisto.setLastRun(next_run) demisto.incidents(incidents) elif command in secops_commands: remove_nulls_from_dictionary(trim_spaces_from_args(args)) return_results(secops_commands[command](client, args)) 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()