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.
"""Test File for GoogleSecOpsCases Integration.""" import json from pathlib import Path from typing import Any from unittest import mock from urllib.parse import urlencode import demistomock as demisto import pytest from CommonServerPython import DemistoException from GoogleSecOpsCases import ( ALERT_ID_DISPLAY, CASE_ALERT_ENTITY_UPDATE_ARGS, CASE_ALERT_UPDATE_ARGS, CASE_ID_DISPLAY, CASE_UPDATE_ARGS, DEFAULT_CASE_LIST_SORT_BY, DEFAULT_CASE_LIST_SORT_ORDER, DEFAULT_PAGE_SIZE, ENDPOINTS, ENTITY_ID_DISPLAY, MAX_FETCH_LIMIT, MAX_PAGE_SIZE, MAX_RETRIES, MESSAGES, SECOPS_OUTPUT_PATHS, VALID_CASE_ALERT_CLOSE_REASONS, VALID_CASE_ALERT_PRIORITIES, VALID_CASE_ALERT_STATUSES, VALID_CASE_CLOSE_REASONS, VALID_CASE_FILTER_LOGIC, VALID_CASE_PRIORITIES, VALID_CASE_SLA_STATUSES, VALID_CASE_STATUSES, VALID_CASE_TYPES, VALID_CASE_WORKFLOW_STATUSES, VALID_EXECUTION_SCOPES, VALID_SORT_ORDERS, Client, convert_time_to_ms, fetch_incidents, gcb_case_alert_customfield_list_command, gcb_case_alert_entity_create_command, gcb_case_alert_entity_get_command, gcb_case_alert_entity_list_command, gcb_case_alert_entity_property_add_command, gcb_case_alert_entity_property_update_command, gcb_case_alert_entity_update_command, gcb_case_alert_get_command, gcb_case_alert_list_command, gcb_case_alert_move_command, gcb_case_alert_recommendation_create_command, gcb_case_alert_recommendation_fetch_command, gcb_case_alert_sla_pause_command, gcb_case_alert_sla_resume_command, gcb_case_alert_sla_set_command, gcb_case_alert_tag_add_command, gcb_case_alert_tag_remove_command, gcb_case_alert_update_command, gcb_case_assign_command, gcb_case_close_command, gcb_case_close_definition_list_command, gcb_case_comment_create_command, gcb_case_comment_list_command, gcb_case_get_command, gcb_case_list_command, gcb_case_priority_change_command, gcb_case_reopen_command, gcb_case_sla_pause_command, gcb_case_sla_resume_command, gcb_case_stage_change_command, gcb_case_stage_definition_list_command, gcb_case_tag_add_command, gcb_case_tag_remove_command, gcb_case_update_command, gcb_playbook_attach_command, gcb_playbook_list_command, main, prepare_alert_entity_filter, test_module as secops_test_module, validate_case_alert_entity_get_args, validate_case_alert_entity_list_args, validate_case_alert_sla_set_args, validate_configuration_parameters, validate_fetch_params, ) PERMISSION_DENIED_TEXT = MESSAGES["PERMISSION_DENIED"] VALID_SERVICE_ACCOUNT = json.dumps({"project_id": "test-project", "type": "service_account"}) VALID_PARAMS: dict[str, Any] = { "credentials": {"password": VALID_SERVICE_ACCOUNT}, "secops_project_instance_id": "test-instance", "region": "us", "secops_project_number": "12345", "url_format": "<chronicle>.<region>.<rep.googleapis.com>", } TEST_DAY_MS = 24 * 60 * 60 * 1000 MY_IP = "0.0.0.1" BASE_URL = "https://chronicle.us.rep.googleapis.com/v1alpha/projects/12345/locations/us/instances/test-instance" TEST_DATA_DIR = Path(__file__).parent def util_load_json(path): """Load file in JSON format.""" with open(TEST_DATA_DIR / path, encoding="utf-8") as f: return json.loads(f.read()) def util_load_text_data(path: str) -> str: """Load a text file.""" with open(TEST_DATA_DIR / path, encoding="utf-8") as f: return f.read() @pytest.fixture def mock_client(mocker): """Fixture for a real Client instance with mocked credentials and retry.""" mocker.patch("GoogleSecOpsCases.service_account.Credentials.from_service_account_info") mocker.patch("GoogleSecOpsCases.skip_proxy") mocker.patch("GoogleSecOpsCases.Client._implement_retry") return Client(params=VALID_PARAMS, proxy=False, disable_ssl=False) def test_module_success(mock_client, requests_mock): """When a valid response is received, test_module should return 'ok'.""" requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASES']}?pageSize=1&expand=products,tasks,tags,closureDetails,sla,alertsSla", json={}, ) result = secops_test_module(mock_client, {}) assert result == "ok" def test_main_test_module(mocker, mock_client): """Test main() routes test-module command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="test-module") mocker.patch.object(demisto, "args", return_value={}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mocker.patch("GoogleSecOpsCases.test_module", return_value="ok") mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_return_results.assert_called_once_with("ok") @pytest.mark.parametrize( "first_fetch", [ "3 days", "7 days", "7 day", "168 hours", "168 hour", "1 week", ], ) def test_module_with_fetch_enabled(mock_client, requests_mock, first_fetch): """When isFetch is enabled, test_module should return 'ok' for valid first_fetch values including 7-day boundary strings.""" fetch_response = util_load_json("test_data/fetch_cases_success_response.json") requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", json=fetch_response) params = {**VALID_PARAMS, "isFetch": True, "first_fetch": first_fetch, "max_fetch": 50} result = secops_test_module(mock_client, params) assert result == "ok" def test_main_unknown_command(mocker, mock_client): """Test main() calls return_error for an unknown command.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="unknown-command") mocker.patch.object(demisto, "args", return_value={}) mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_return_error = mocker.patch("GoogleSecOpsCases.return_error") main() mock_return_error.assert_called_once() assert "unknown-command" in mock_return_error.call_args[0][0] def test_validate_configuration_parameters_success(): """When valid parameters are provided, validate_configuration_parameters should pass without errors.""" params = { "credentials": {"password": VALID_SERVICE_ACCOUNT}, "secops_project_instance_id": "dummy_instance_id", "region": "us", } validate_configuration_parameters(params) def test_validate_configuration_parameters_invalid_service_account_json(): """When invalid service account JSON is provided, should raise ValueError.""" params = { "credentials": {"password": '{"key","value"}'}, "secops_project_instance_id": "dummy_instance_id", } with pytest.raises(ValueError) as error: validate_configuration_parameters(params) assert str(error.value) == MESSAGES["INVALID_SERVICE_ACCOUNT_JSON"] @pytest.mark.parametrize( "params, expected_error_message", [ ({}, MESSAGES["MISSING_PROJECT_INSTANCE_ID"]), ( {"secops_project_instance_id": "dummy", "region": "other"}, MESSAGES["MISSING_REGION"], ), ( { "secops_project_instance_id": "dummy", "credentials": {"password": "{}"}, "secops_project_number": "invalid_number", }, MESSAGES["INVALID_PROJECT_NUMBER"], ), ( { "secops_project_instance_id": "dummy", "credentials": {"password": "{}"}, "secops_project_number": "0", }, MESSAGES["INVALID_PROJECT_NUMBER"], ), ( { "secops_project_instance_id": "dummy", "credentials": {"password": "{}"}, "secops_project_number": "-123", }, MESSAGES["INVALID_PROJECT_NUMBER"], ), ], ) def test_validate_configuration_parameters_invalid_params(params, expected_error_message): """Test validate_configuration_parameters with invalid parameters.""" with pytest.raises(ValueError) as error: validate_configuration_parameters(params) assert str(error.value) == expected_error_message @pytest.mark.parametrize( "proxy, handle_proxy_return, expected_https_proxy, url_format, use_new_url_format", [ (False, None, None, "<chronicle>.<region>.<rep.googleapis.com>", True), ( True, {"https": "demo.com:8080"}, "https://demo.com:8080", "<region>.<chronicle>.googleapis.com>", False, ), ], ) def test_client_init(mocker, proxy, handle_proxy_return, expected_https_proxy, url_format, use_new_url_format): """Test Client.__init__ with and without proxy.""" mocker.patch("GoogleSecOpsCases.service_account.Credentials.from_service_account_info") mocker.patch("GoogleSecOpsCases.auth_requests.AuthorizedSession") mocker.patch("GoogleSecOpsCases.Client._implement_retry") if proxy: mocker.patch("GoogleSecOpsCases.handle_proxy", return_value=handle_proxy_return) else: mocker.patch("GoogleSecOpsCases.skip_proxy") params = {**VALID_PARAMS, "url_format": url_format} google_secops_client = Client(params=params, proxy=proxy, disable_ssl=False) assert google_secops_client.use_new_url_format is use_new_url_format if expected_https_proxy: assert google_secops_client.proxy_info["https"] == expected_https_proxy @pytest.mark.parametrize( "extra_params, expected_attr, expected_value", [ ({"region": "other", "other_region": "eu"}, "project_location", "eu"), ({"secops_project_number": ""}, "project_number", "test-project"), ], ) def test_client_init_special_params(mocker, extra_params, expected_attr, expected_value): """Test Client.__init__ other_region and project_number fallback branches.""" mocker.patch("GoogleSecOpsCases.service_account.Credentials.from_service_account_info") mocker.patch("GoogleSecOpsCases.auth_requests.AuthorizedSession") mocker.patch("GoogleSecOpsCases.skip_proxy") mocker.patch("GoogleSecOpsCases.Client._implement_retry") google_secops_client = Client(params={**VALID_PARAMS, **extra_params}, proxy=False, disable_ssl=False) assert getattr(google_secops_client, expected_attr) == expected_value def test_implement_retry_ssl_disabled(mocker): """Test _implement_retry mounts SSLAdapter when disable_ssl=True and Python >= 3.10.""" mocker.patch("GoogleSecOpsCases.service_account.Credentials.from_service_account_info") mocker.patch("GoogleSecOpsCases.auth_requests.AuthorizedSession") mocker.patch("GoogleSecOpsCases.skip_proxy") mock_ssl_adapter = mocker.patch("GoogleSecOpsCases.SSLAdapter") mocker.patch("GoogleSecOpsCases.IS_PY3", True) mocker.patch("GoogleSecOpsCases.PY_VER_MINOR", 10) google_secops_client = Client(params=VALID_PARAMS, proxy=False, disable_ssl=True) mock_ssl_adapter.assert_called_once() google_secops_client.http_client.mount.assert_called_once() def test_client_init_empty_https_proxy_raises(mocker): """Test Client.__init__ raises DemistoException when handle_proxy returns empty https value (line 193).""" mocker.patch("GoogleSecOpsCases.service_account.Credentials.from_service_account_info") mocker.patch("GoogleSecOpsCases.auth_requests.AuthorizedSession") mocker.patch("GoogleSecOpsCases.Client._implement_retry") mocker.patch("GoogleSecOpsCases.handle_proxy", return_value={"https": ""}) with pytest.raises(DemistoException, match="https proxy value is empty"): Client(params=VALID_PARAMS, proxy=True, disable_ssl=False) def test_implement_retry_ssl_enabled(mocker): """Test _implement_retry mounts plain HTTPAdapter when disable_ssl=False (line 282).""" mocker.patch("GoogleSecOpsCases.service_account.Credentials.from_service_account_info") mocker.patch("GoogleSecOpsCases.auth_requests.AuthorizedSession") mocker.patch("GoogleSecOpsCases.skip_proxy") mock_http_adapter = mocker.patch("GoogleSecOpsCases.HTTPAdapter") google_secops_client = Client(params=VALID_PARAMS, proxy=False, disable_ssl=False) mock_http_adapter.assert_called_once() google_secops_client.http_client.mount.assert_called_once() def test_implement_retry_ssl_disabled_old_python(mocker): """Test _implement_retry falls back to HTTPAdapter when IS_PY3=False or PY_VER_MINOR < 10 (line 286).""" mocker.patch("GoogleSecOpsCases.service_account.Credentials.from_service_account_info") mocker.patch("GoogleSecOpsCases.auth_requests.AuthorizedSession") mocker.patch("GoogleSecOpsCases.skip_proxy") mock_ssl_adapter = mocker.patch("GoogleSecOpsCases.SSLAdapter") mocker.patch("GoogleSecOpsCases.IS_PY3", True) mocker.patch("GoogleSecOpsCases.PY_VER_MINOR", 9) google_secops_client = Client(params=VALID_PARAMS, proxy=False, disable_ssl=True) mock_ssl_adapter.assert_not_called() google_secops_client.http_client.mount.assert_called_once() def test_implement_retry_name_error(mocker): """Test _implement_retry swallows NameError when Retry is not available (lines 290-291).""" mocker.patch("GoogleSecOpsCases.service_account.Credentials.from_service_account_info") mocker.patch("GoogleSecOpsCases.auth_requests.AuthorizedSession") mocker.patch("GoogleSecOpsCases.skip_proxy") mocker.patch("GoogleSecOpsCases.Retry", side_effect=NameError("Retry not defined")) mock_debug = mocker.patch("GoogleSecOpsCases.demisto.debug") Client(params=VALID_PARAMS, proxy=False, disable_ssl=False) mock_debug.assert_called_once() assert "_implement_retry: NameError" in mock_debug.call_args[0][0] @pytest.mark.parametrize( "error_input, region, expected", [ (json.dumps({"error": {"code": 403, "message": "Forbidden"}}), "us", MESSAGES["PERMISSION_DENIED"]), (json.dumps([{"error": {"message": "list error message", "code": 400}}]), "us", "list error message"), ( "404 Not Found plain text", "other", 'Invalid response from Google SecOps API. Check the provided "Other Region" parameter.', ), ("plain text error", "us", "Invalid response received from SecOps API. Response not in JSON format."), ( json.dumps({"error": {"code": 409, "message": "Alert is already closed.", "status": "FAILED_PRECONDITION"}}), "us", "Alert is already closed.\nStatus: FAILED_PRECONDITION", ), ], ) def test_parse_error_message(error_input, region, expected): """Test _parse_error_message across all branches.""" assert Client._parse_error_message(error_input, region) == expected @pytest.mark.parametrize( "use_new_url_format, location, expected_url_part", [ (True, "us", "chronicle.us.rep.googleapis.com"), (False, "eu", "eu-chronicle.googleapis.com"), ], ) def test_create_url_path(mock_client, use_new_url_format, location, expected_url_part): """Test _create_url_path uses the correct URL format.""" mock_client.project_number = "12345" mock_client.project_location = location mock_client.project_instance_id = "test-instance" mock_client.use_new_url_format = use_new_url_format result = Client._create_url_path(mock_client) assert expected_url_part in result assert f"projects/12345/locations/{location}/instances/test-instance" in result @pytest.mark.parametrize( "status_code, expected_error_text", [ (500, MESSAGES["INTERNAL_SERVER_ERROR"].format(500, MAX_RETRIES, "error")), (429, MESSAGES["RATE_LIMIT_EXCEEDED"].format(429, MAX_RETRIES, "error")), (400, MESSAGES["HTTP_ERROR"].format(400, "error")), (401, MESSAGES["HTTP_ERROR"].format(401, "error")), ], ) def test_validate_response_error_status_codes(mock_client, requests_mock, status_code, expected_error_text): """Test validate_response raises ValueError for error status codes.""" requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", json={"error": {"message": "error"}}, status_code=status_code) with pytest.raises(ValueError) as error: mock_client.validate_response(ENDPOINTS["CASES"]) assert str(error.value) == expected_error_text def test_validate_response_non_json_body(mock_client, requests_mock): """Test validate_response raises ValueError when response body is not valid JSON.""" requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", text="not-json", status_code=200) with pytest.raises(ValueError) as error: mock_client.validate_response(ENDPOINTS["CASES"]) assert str(error.value) == MESSAGES["INVALID_JSON_RESPONSE"] @pytest.mark.parametrize( "allow_empty_response, expected_result, expected_error", [ (True, {}, None), (False, None, MESSAGES["EMPTY_RESPONSE"].format(200)), ], ) def test_validate_response_empty_body(mock_client, requests_mock, allow_empty_response, expected_result, expected_error): """Test validate_response with empty body — returns {} when allowed, raises ValueError when not.""" requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", text="", status_code=200) if expected_error: with pytest.raises(ValueError) as error: mock_client.validate_response(ENDPOINTS["CASES"], allow_empty_response=allow_empty_response) assert str(error.value) == expected_error else: result = mock_client.validate_response(ENDPOINTS["CASES"], allow_empty_response=allow_empty_response) assert result == expected_result def test_fetch_incidents_success_without_last_run(mock_client, requests_mock): """Test fetch_incidents returns incidents and correct next_state when last_run is empty.""" fetch_response = util_load_json("test_data/fetch_cases_success_response.json") expected_incidents = util_load_json("test_data/fetch_cases_incidents.json") requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", json=fetch_response) params = { **VALID_PARAMS, "first_fetch": "2026-01-01T00:00:00Z", "max_fetch": MAX_PAGE_SIZE, "case_priorities": "MEDIUM", "case_statuses": "OPENED", "case_environments": "Default", "case_tags": "phishing", "case_filter_logic": "AND", } incidents, next_state = fetch_incidents(mock_client, params, last_run={}) assert incidents == expected_incidents assert "cases" in next_state cases_state = next_state["cases"] assert cases_state.get("page_token") == "dummy_next_page_token" assert cases_state.get("filter_hash") == "31265ed8b21176b0403f54e4e92bbcc2" assert cases_state.get("last_case_create_time") == "2026-05-07T01:39:18.871000Z" ingested_ids = cases_state["ingested_case_ids"] assert len(ingested_ids) == 3 assert "101" in ingested_ids assert "102" in ingested_ids assert "103" in ingested_ids def test_fetch_incidents_success_with_last_run(mock_client, requests_mock): """Test fetch_incidents updates state correctly when last_run already has start_time and page_token.""" fetch_response = util_load_json("test_data/fetch_cases_success_response.json") expected_incidents = util_load_json("test_data/fetch_cases_incidents.json") requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", json=fetch_response) last_run = { "cases": { "start_time": "2026-05-01T00:00:00.000000Z", "page_token": "existing_page_cursor", "ingested_case_ids": [], "filter_hash": "26379b4441bae5bbf56de66366e5b0cf", } } params = {**VALID_PARAMS, "first_fetch": "3 days", "max_fetch": 10, "case_tags": "phishing", "case_filter_logic": "or"} incidents, next_state = fetch_incidents(mock_client, params, last_run=last_run) assert incidents == expected_incidents cases_state = next_state["cases"] assert cases_state.get("start_time") == "2026-05-01T00:00:00.000000Z" assert cases_state.get("page_token") == "dummy_next_page_token" ingested_ids = cases_state["ingested_case_ids"] assert len(ingested_ids) == 3 assert "101" in ingested_ids assert "102" in ingested_ids assert "103" in ingested_ids def test_fetch_incidents_with_duplicates(mock_client, requests_mock): """Test that already-ingested case IDs are skipped and not returned as incidents.""" fetch_response = util_load_json("test_data/fetch_cases_success_response.json") expected_incidents = util_load_json("test_data/fetch_cases_incidents.json") requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", json=fetch_response) last_run = {"cases": {"start_time": "2026-05-01T00:00:00.000000Z", "ingested_case_ids": ["101"]}} params = {**VALID_PARAMS, "first_fetch": "3 days", "max_fetch": 10} incidents, next_state = fetch_incidents(mock_client, params, last_run=last_run) assert incidents == expected_incidents[1:] # case 101 skipped ingested_ids = next_state["cases"]["ingested_case_ids"] assert len(ingested_ids) == 3 assert "101" in ingested_ids assert "102" in ingested_ids assert "103" in ingested_ids def test_fetch_incidents_with_no_page_token(mock_client, requests_mock): """Test fetch_incidents does not set page_token in next_state when the response has no next page.""" fetch_response = util_load_json("test_data/fetch_cases_success_response.json") fetch_response_no_token = {k: v for k, v in fetch_response.items() if k != "nextPageToken"} requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", json=fetch_response_no_token) last_run = {"cases": {"start_time": "2026-05-01T00:00:00.000000Z", "page_token": "old_page_cursor", "ingested_case_ids": []}} params = {**VALID_PARAMS, "first_fetch": "3 days", "max_fetch": 10} incidents, next_state = fetch_incidents(mock_client, params, last_run=last_run) assert len(incidents) == 3 assert "page_token" not in next_state["cases"] def test_fetch_incidents_empty_response(mock_client, requests_mock): """Test fetch_incidents returns empty incidents list and preserves existing last_run state.""" requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", json={"cases": []}) last_run = {"cases": {"start_time": "2026-05-01T00:00:00.000000Z", "page_token": "some_cursor", "ingested_case_ids": ["101"]}} params = {**VALID_PARAMS, "first_fetch": "3 days", "max_fetch": 10} incidents, next_state = fetch_incidents(mock_client, params, last_run=last_run) assert incidents == [] cases_state = next_state["cases"] assert cases_state.get("start_time") == "2026-05-01T00:00:00.000000Z" assert cases_state.get("ingested_case_ids") == ["101"] assert "page_token" not in cases_state def test_fetch_incidents_filter_config_changed(mock_client, requests_mock): """Test that page_token is reset and start_time uses last_case_create_time when filter params change.""" fetch_response = util_load_json("test_data/fetch_cases_success_response.json") requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", json=fetch_response) last_run = { "cases": { "start_time": "2026-05-01T00:00:00.000000Z", "page_token": "stale_page_cursor", "ingested_case_ids": ["99"], "filter_hash": "old_hash_that_will_not_match", "last_case_create_time": "2026-05-03T00:00:00.000000Z", } } params = { **VALID_PARAMS, "first_fetch": "3 days", "max_fetch": 10, "case_priorities": "HIGH", "case_statuses": "OPENED", "case_filter_logic": "AND", } incidents, next_state = fetch_incidents(mock_client, params, last_run=last_run) cases_state = next_state["cases"] assert len(incidents) == 3 assert cases_state.get("page_token") == "dummy_next_page_token" assert cases_state.get("start_time") == "2026-05-03T00:00:00.000000Z" assert cases_state.get("last_case_create_time") == "2026-05-07T01:39:18.871000Z" assert cases_state.get("filter_hash") == "5c2a12a431235dfd53483fe85e788bfb" def test_main_fetch_incidents(mocker, mock_client, requests_mock): """Test main() routes fetch-incidents command correctly.""" fetch_response = util_load_json("test_data/fetch_cases_success_response.json") expected_incidents = util_load_json("test_data/fetch_cases_incidents.json") requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", json=fetch_response) mocker.patch.object(demisto, "params", return_value={**VALID_PARAMS, "first_fetch": "3 days", "max_fetch": 50}) mocker.patch.object(demisto, "command", return_value="fetch-incidents") mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_set_last_run = mocker.patch.object(demisto, "setLastRun") mock_incidents = mocker.patch.object(demisto, "incidents") main() mock_set_last_run.assert_called_once() mock_incidents.assert_called_once_with(expected_incidents) @pytest.mark.parametrize( "params, expected_error", [ ( {"case_priorities": "INVALID_PRIORITY"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("case_priorities", ", ".join(VALID_CASE_PRIORITIES)), ), ( {"case_statuses": "INVALID_STATUS"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("case_statuses", ", ".join(VALID_CASE_STATUSES)), ), ( {"case_filter_logic": "XOR"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("case_filter_logic", ", ".join(VALID_CASE_FILTER_LOGIC)), ), ( {"first_fetch": "2200-01-01T00:00:00Z"}, MESSAGES["FUTURE_DATE"], ), ( {"first_fetch": "10 days"}, MESSAGES["INVALID_FIRST_FETCH"].format("10 days"), ), ( {"max_fetch": 0}, MESSAGES["INVALID_MAX_FETCH"].format(value=0, max_limit=MAX_FETCH_LIMIT), ), ( {"max_fetch": -1}, MESSAGES["INVALID_MAX_FETCH"].format(value=-1, max_limit=MAX_FETCH_LIMIT), ), ( {"max_fetch": "abc"}, MESSAGES["INVALID_MAX_FETCH"].format(value="abc", max_limit=MAX_FETCH_LIMIT), ), ( {"max_fetch": 201}, MESSAGES["INVALID_MAX_FETCH"].format(value=201, max_limit=MAX_FETCH_LIMIT), ), ( {"max_fetch": 300}, MESSAGES["INVALID_MAX_FETCH"].format(value=300, max_limit=MAX_FETCH_LIMIT), ), ], ) def test_validate_fetch_params_invalid_values(params, expected_error): """Test validate_fetch_params raises ValueError for invalid parameter values when is_test=True.""" with pytest.raises(ValueError) as exc_info: validate_fetch_params(params, is_test=True) assert expected_error in str(exc_info.value) def test_gcb_case_list_command_success(mock_client, requests_mock): """Success with all individual filter args and mixed assignees (@SOC role + email).""" user_email = "active.user@example.com" user_id = "00000000-0000-0000-0000-000000000111" soar_users_response = util_load_json("test_data/legacy_soar_users_response.json") emails = [user_email, "not_found_email"] email_conditions = " OR ".join(f"email='{e}'" for e in emails) soar_users_qs = urlencode({"pageSize": len(emails), "filter": f"({email_conditions}) AND accountState='ACTIVE'"}) mock_response = util_load_json("test_data/case_list_success_response.json") expected_outputs = util_load_json("test_data/case_list_context.json") expected_hr = util_load_text_data("test_data/case_list_hr.md") case_filter = " AND ".join( [ '(priority="PRIORITY_HIGH" OR priority="PRIORITY_CRITICAL")', '(status="OPENED")', '(environment="Production")', '(displayName="test case")', '(type="EXTERNAL")', '(stage="Triage")', '(source="Server")', f'(assignee="@SOC" OR assignee="{user_id}")', '(workflowStatus="IN_PROGRESS")', '(sla.expirationStatus="OPEN_SLA")', '(alertsSla.expirationStatus="PASSED_DUE")', 'any(tags.displayName,"malware","phishing")', 'any(products.displayName,"CrowdStrike")', "important=true", "incident=false", ] ) cases_qs = urlencode( { "pageSize": 10, "expand": "products,tasks,tags,closureDetails,sla,alertsSla", "filter": case_filter, "orderBy": "createTime desc", } ) requests_mock.get(f"{BASE_URL}{ENDPOINTS['LEGACY_SOAR_USERS']}?{soar_users_qs}", json=soar_users_response) requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}?{cases_qs}", json=mock_response) args = { "page_size": "10", "sort_by": "createTime", "sort_order": "Desc", "priority": "HIGH,CRITICAL", "status": "OPENED", "environment": "Production", "tags": "malware,phishing", "display_name": "test case", "type": "EXTERNAL", "stage": "Triage", "source": "Server", "assignee": f"@SOC,{user_email}, , not_found_email", "products": "CrowdStrike", "important": "true", "incident": "false", "workflow_status": "IN_PROGRESS", "sla": "OPEN_SLA", "alerts_sla": "PASSED_DUE", "filter_logic": "AND", } result = gcb_case_list_command(mock_client, args) assert result.outputs == expected_outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_case_list_command_advanced_filter_success(mock_client, requests_mock): """Success scenario with advanced_filter: individual filters are ignored, raw AIP-160 filter is used.""" mock_response = util_load_json("test_data/case_list_success_response.json") requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}", json=mock_response) args = {"advanced_filter": 'priority="HIGH" AND stage="Triage"', "priority": "LOW", "status": "CLOSED"} result = gcb_case_list_command(mock_client, args) assert result.raw_response == mock_response assert result.outputs is not None def test_gcb_case_list_command_empty_response(mock_client, requests_mock): """Empty response with page_token and create/update time range filters.""" case_filter = " AND ".join( ["createTime>=1777593600000", "createTime<=1780272000000", "updateTime>=1778803200000", "updateTime<=1780617600000"] ) cases_qs = urlencode( { "pageSize": DEFAULT_PAGE_SIZE, "pageToken": "token123", "expand": "products,tasks,tags,closureDetails,sla,alertsSla", "filter": case_filter, "orderBy": f"{DEFAULT_CASE_LIST_SORT_BY} {DEFAULT_CASE_LIST_SORT_ORDER.lower()}", } ) requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASES']}?{cases_qs}", json={}) args = { "page_token": "token123", "create_start_time": "2026-05-01T00:00:00Z", "create_end_time": "2026-06-01T00:00:00Z", "update_start_time": "2026-05-15T00:00:00Z", "update_end_time": "2026-06-05T00:00:00Z", } result = gcb_case_list_command(mock_client, args) assert result.raw_response == {} assert "No cases found" in result.readable_output @pytest.mark.parametrize( "args, expected_error_message", [ ( {"page_size": "0"}, MESSAGES["INVALID_INT_RANGE"].format(0, "page_size", 1, MAX_PAGE_SIZE), ), ( {"page_size": "-1"}, MESSAGES["INVALID_INT_RANGE"].format(-1, "page_size", 1, MAX_PAGE_SIZE), ), ( {"page_size": "1001"}, MESSAGES["INVALID_INT_RANGE"].format(1001, "page_size", 1, MAX_PAGE_SIZE), ), ( {"sort_order": "Invalid"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("sort_order", ", ".join(VALID_SORT_ORDERS)), ), ( {"priority": "INVALID"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("priority", ", ".join(VALID_CASE_PRIORITIES)), ), ( {"status": "INVALID_STATUS"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("status", ", ".join(VALID_CASE_STATUSES)), ), ( {"type": "INVALID_TYPE"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("type", ", ".join(VALID_CASE_TYPES)), ), ( {"workflow_status": "INVALID_WF"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("workflow_status", ", ".join(VALID_CASE_WORKFLOW_STATUSES)), ), ( {"sla": "INVALID_SLA"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("sla", ", ".join(VALID_CASE_SLA_STATUSES)), ), ( {"alerts_sla": "INVALID_SLA"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("alerts_sla", ", ".join(VALID_CASE_SLA_STATUSES)), ), ( {"filter_logic": "XOR"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("filter_logic", ", ".join(VALID_CASE_FILTER_LOGIC)), ), ( {"create_start_time": "2026-06-01T00:00:00Z", "create_end_time": "2026-05-01T00:00:00Z"}, MESSAGES["INVALID_DATE_RANGE"].format("create_start_time", "create_end_time"), ), ( {"update_start_time": "2026-06-05T00:00:00Z", "update_end_time": "2026-05-15T00:00:00Z"}, MESSAGES["INVALID_DATE_RANGE"].format("update_start_time", "update_end_time"), ), ], ) def test_gcb_case_list_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_list_command raises ValueError for invalid argument values.""" with pytest.raises(ValueError) as error: gcb_case_list_command(mock_client, args) assert str(error.value) == expected_error_message def test_gcb_case_get_command_success(mock_client, requests_mock): """Test gcb_case_get_command with a successful API response.""" test_data = util_load_json("test_data/case_get_response.json") expected_hr = util_load_text_data("test_data/case_get_hr.md") requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASE'].format(case_id='1001')}", json=test_data.get("raw_response")) result = gcb_case_get_command(mock_client, {"case_id": "1001"}) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == test_data.get("outputs") assert result.raw_response == test_data.get("raw_response") assert result.readable_output == expected_hr def test_gcb_case_get_command_api_error(mock_client, requests_mock): """Test gcb_case_get_command when API returns a 404 error for a non-existent case.""" case_id = "999999" error_message = "Case not found" requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE'].format(case_id=case_id)}", json={"error": {"message": error_message}}, status_code=404, ) with pytest.raises(ValueError) as error: gcb_case_get_command(mock_client, {"case_id": case_id}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(404, error_message) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY)), ({"case_id": "-1"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-1", CASE_ID_DISPLAY)), ({"case_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY)), ], ) def test_gcb_case_get_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_get_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_get_command(mock_client, args) assert str(error.value) == expected_error_message def test_gcb_case_get_command_empty_response(mock_client, requests_mock): """Test gcb_case_get_command when API returns empty response.""" requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASE'].format(case_id='1001')}", json={}) result = gcb_case_get_command(mock_client, {"case_id": "1001"}) assert result.readable_output == MESSAGES["NO_RECORDS_FOUND"].format("case information") assert result.outputs is None assert result.raw_response is None @pytest.mark.parametrize( "args, expected_update_mask_fields", [ ( { "case_id": "1001", "display_name": "XSOAR 1 Testing", "description": "Test description", "important": "true", "incident": "false", }, {"displayname", "description", "important", "incident"}, ), ( {"case_id": "1001", "display_name": "XSOAR 1 Testing"}, {"displayname"}, ), ( {"case_id": "1001", "important": "true"}, {"important"}, ), ], ) def test_gcb_case_update_command_success(mock_client, requests_mock, args, expected_update_mask_fields): """Test gcb_case_update_command: all-fields, partial (display_name only), and boolean-only (important only).""" raw_response = util_load_json("test_data/case_update_raw_response.json") expected_context = util_load_json("test_data/case_update_context.json") expected_hr = util_load_text_data("test_data/case_update_hr.md") requests_mock.patch(f"{BASE_URL}{ENDPOINTS['CASE'].format(case_id='1001')}", json=raw_response) result = gcb_case_update_command(mock_client, args) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_context assert result.raw_response == raw_response assert result.readable_output == expected_hr update_mask_value = requests_mock.last_request.qs["updatemask"][0] assert set(update_mask_value.split(",")) == expected_update_mask_fields def test_gcb_case_update_command_api_error(mock_client, requests_mock): """Test gcb_case_update_command when API returns a 404 error for a non-existent case.""" case_id = "999999" error_message = "Case not found" requests_mock.patch( f"{BASE_URL}{ENDPOINTS['CASE'].format(case_id=case_id)}", json={"error": {"message": error_message}}, status_code=404, ) with pytest.raises(ValueError) as error: gcb_case_update_command(mock_client, {"case_id": case_id, "display_name": "Test"}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(404, error_message) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY)), ({"case_id": "-1"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-1", CASE_ID_DISPLAY)), ({"case_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY)), ( {"case_id": "1001"}, MESSAGES["AT_LEAST_ONE_REQUIRED"].format(", ".join(CASE_UPDATE_ARGS)), ), ], ) def test_gcb_case_update_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_update_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_update_command(mock_client, args) assert str(error.value) == expected_error_message def test_gcb_case_tag_add_command_success(mock_client, requests_mock): """Test gcb_case_tag_add_command with a successful response.""" requests_mock.post(f"{BASE_URL}{ENDPOINTS['CASES_BULK_ADD_TAG']}", text="", status_code=200) expected_outputs = [ {"caseId": "1001", "recentlyAddedTags": ["malware", "phishing"]}, {"caseId": "1002", "recentlyAddedTags": ["malware", "phishing"]}, ] result = gcb_case_tag_add_command(mock_client, {"case_ids": "1001,1002", "tags": "malware,phishing"}) assert result.readable_output == "Tags malware, phishing successfully added to cases 1001, 1002." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_tag_add_command_api_error(mock_client, requests_mock): """Test gcb_case_tag_add_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASES_BULK_ADD_TAG']}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_tag_add_command(mock_client, {"case_ids": "1001", "tags": "malware"}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(403, PERMISSION_DENIED_TEXT) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_ids")), ({"case_ids": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("tags")), ({"case_ids": "abc", "tags": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY)), ({"case_ids": "-5", "tags": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "-5", CASE_ID_DISPLAY)), ({"case_ids": "0", "tags": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "0", CASE_ID_DISPLAY)), ( {"case_ids": "1001,abc,1002", "tags": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY), ), ], ) def test_gcb_case_tag_add_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_tag_add_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_tag_add_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_tag_add_command_success(mocker, mock_client): """Test main() routes gcb-case-tag-add command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-tag-add") mocker.patch.object(demisto, "args", return_value={"case_ids": "1001", "tags": "malware"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_tag_add_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_tag_remove_command_success(mock_client, requests_mock): """Test gcb_case_tag_remove_command with a successful response.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASES_REMOVE_TAG'].format(case_id='1001')}", text="", status_code=200, ) expected_outputs = {"caseId": "1001", "recentlyRemovedTag": "malware"} result = gcb_case_tag_remove_command(mock_client, {"case_id": "1001", "tag": "malware"}) assert result.readable_output == "Tag malware successfully removed from case 1001." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_tag_remove_command_api_error(mock_client, requests_mock): """Test gcb_case_tag_remove_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASES_REMOVE_TAG'].format(case_id='1001')}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_tag_remove_command(mock_client, {"case_id": "1001", "tag": "malware"}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(403, PERMISSION_DENIED_TEXT) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("tag")), ( {"case_id": "abc,def", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc,def", CASE_ID_DISPLAY), ), ({"case_id": "-5", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY)), ({"case_id": "0", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY)), ], ) def test_gcb_case_tag_remove_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_tag_remove_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_tag_remove_command(mock_client, args) assert str(error.value) == expected_error_message @pytest.mark.parametrize( "input_priority, expected_priority", [ ("info", "INFO"), ("Low", "LOW"), ("MEDIUM", "MEDIUM"), ("High", "HIGH"), ("critical", "CRITICAL"), ], ) def test_gcb_case_priority_change_command_success(mock_client, requests_mock, input_priority, expected_priority): """Test gcb_case_priority_change_command succeeds for all supported priorities with mixed-case input.""" requests_mock.post(f"{BASE_URL}{ENDPOINTS['CASES_BULK_CHANGE_PRIORITY']}", text="", status_code=200) expected_outputs = [ {"caseId": "1001", "priority": expected_priority}, {"caseId": "1002", "priority": expected_priority}, ] result = gcb_case_priority_change_command(mock_client, {"case_ids": "1001,1002", "priority": input_priority}) assert result.readable_output == f"Priority of cases 1001, 1002 successfully changed to {expected_priority}." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_priority_change_command_api_error(mock_client, requests_mock): """Test gcb_case_priority_change_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASES_BULK_CHANGE_PRIORITY']}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_priority_change_command(mock_client, {"case_ids": "1001", "priority": "CRITICAL"}) assert PERMISSION_DENIED_TEXT in str(error.value) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_ids")), ({"case_ids": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("priority")), ( {"case_ids": "abc", "priority": "CRITICAL"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY), ), ( {"case_ids": "-5", "priority": "CRITICAL"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "-5", CASE_ID_DISPLAY), ), ( {"case_ids": "0", "priority": "CRITICAL"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "0", CASE_ID_DISPLAY), ), ( {"case_ids": "1001,abc,1002", "priority": "CRITICAL"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY), ), ( {"case_ids": "1001", "priority": "INVALID"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("priority", ", ".join(VALID_CASE_PRIORITIES)), ), ], ) def test_gcb_case_priority_change_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_priority_change_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_priority_change_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_priority_change_command_success(mocker, mock_client): """Test main() routes gcb-case-priority-change command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-priority-change") mocker.patch.object(demisto, "args", return_value={"case_ids": "1001", "priority": "CRITICAL"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_priority_change_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_stage_definition_list_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_stage_definition_list_command should return display names.""" base_name = "projects/google-001/locations/us/instances/00000000-0000-0000-0000-000000000001/caseStageDefinitions" mock_response = { "caseStageDefinitions": [ {"name": f"{base_name}/1", "displayName": "Triage", "order": 1}, {"name": f"{base_name}/2", "displayName": "Assessment", "order": 2}, {"name": f"{base_name}/3", "displayName": "Investigation", "order": 3}, ] } requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_STAGE_DEFINITIONS']}?pageSize={MAX_PAGE_SIZE}&orderBy=order", json=mock_response, ) result = gcb_case_stage_definition_list_command(mock_client, {}) assert result.outputs_prefix == "GoogleSecOps.CaseStageDefinition" assert result.outputs == ["Triage", "Assessment", "Investigation"] assert "Triage, Assessment, Investigation" in result.readable_output def test_main_gcb_case_stage_definition_list(mocker, mock_client): """Test main() routes gcb-case-stage-definition-list command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-stage-definition-list") mocker.patch.object(demisto, "args", return_value={}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_stage_definition_list_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) @pytest.mark.parametrize("input_stage", ["triage", "Incident", "Investigation", "Assessment"]) def test_gcb_case_stage_change_command_success(mock_client, requests_mock, input_stage): """Test gcb_case_stage_change_command succeeds for stages with mixed-case input.""" requests_mock.post(f"{BASE_URL}{ENDPOINTS['CASES_BULK_CHANGE_STAGE']}", text="", status_code=200) expected_outputs = [ {"caseId": "1001", "stage": input_stage}, {"caseId": "1002", "stage": input_stage}, ] result = gcb_case_stage_change_command(mock_client, {"case_ids": "1001,1002", "stage": input_stage}) assert result.readable_output == f"Stage of cases 1001, 1002 successfully changed to {input_stage}." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_stage_change_command_api_error(mock_client, requests_mock): """Test gcb_case_stage_change_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASES_BULK_CHANGE_STAGE']}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_stage_change_command(mock_client, {"case_ids": "1001", "stage": "Investigation"}) assert PERMISSION_DENIED_TEXT in str(error.value) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_ids")), ({"case_ids": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("stage")), ({"case_ids": "abc", "stage": "Triage"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY)), ({"case_ids": "-5", "stage": "Triage"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "-5", CASE_ID_DISPLAY)), ({"case_ids": "0", "stage": "Triage"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "0", CASE_ID_DISPLAY)), ( {"case_ids": "1001,abc,1002", "stage": "Triage"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY), ), ], ) def test_gcb_case_stage_change_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_stage_change_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_stage_change_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_stage_change_command_success(mocker, mock_client): """Test main() routes gcb-case-stage-change command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-stage-change") mocker.patch.object(demisto, "args", return_value={"case_ids": "1001", "stage": "Investigation"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_stage_change_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_reopen_command_success(mock_client, requests_mock): """Test gcb_case_reopen_command succeeds for valid case IDs.""" requests_mock.post(f"{BASE_URL}{ENDPOINTS['CASES_BULK_REOPEN']}", text="", status_code=200) expected_outputs = [ {"caseId": "1001", "status": "OPENED"}, {"caseId": "1002", "status": "OPENED"}, ] result = gcb_case_reopen_command(mock_client, {"case_ids": "1001,1002", "reopen_comment": "Reopening due to new evidence."}) assert result.readable_output == "Cases 1001, 1002 successfully reopened." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_reopen_command_api_error(mock_client, requests_mock): """Test gcb_case_reopen_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASES_BULK_REOPEN']}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_reopen_command(mock_client, {"case_ids": "1001", "reopen_comment": "Reopening due to new evidence."}) assert PERMISSION_DENIED_TEXT in str(error.value) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_ids")), ({"case_ids": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("reopen_comment")), ( {"case_ids": "abc", "reopen_comment": "test"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY), ), ( {"case_ids": "-5", "reopen_comment": "test"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "-5", CASE_ID_DISPLAY), ), ( {"case_ids": "0", "reopen_comment": "test"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "0", CASE_ID_DISPLAY), ), ( {"case_ids": "1001,abc,1002", "reopen_comment": "test"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY), ), ], ) def test_gcb_case_reopen_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_reopen_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_reopen_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_reopen_command_success(mocker, mock_client): """Test main() routes gcb-case-reopen command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-reopen") mocker.patch.object(demisto, "args", return_value={"case_ids": "1001", "reopen_comment": "Reopening due to new evidence."}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_reopen_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_close_definition_list_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_close_definition_list_command should return case close definitions.""" mock_response = util_load_json("test_data/case_close_definitions.json") expected_hr = util_load_text_data("test_data/case_close_definitions_hr.md") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_CLOSE_DEFINITIONS']}?pageSize={MAX_PAGE_SIZE}&orderBy=closeReason", json=mock_response, ) result = gcb_case_close_definition_list_command(mock_client, {}) assert result.outputs_prefix == "GoogleSecOps.CaseCloseDefinition" assert result.outputs == mock_response["caseCloseDefinitions"] assert result.readable_output == expected_hr def test_gcb_case_close_definition_list_command_empty_response(mock_client, requests_mock): """When empty response is received, gcb_case_close_definition_list_command should return appropriate message.""" mock_response = {"caseCloseDefinitions": []} requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_CLOSE_DEFINITIONS']}?pageSize=1000&orderBy=closeReason", json=mock_response, ) result = gcb_case_close_definition_list_command(mock_client, {}) assert result.outputs_prefix == "GoogleSecOps.CaseCloseDefinition" assert result.outputs == [] assert "No case close definitions found" in result.readable_output def test_main_gcb_case_close_definition_list(mocker, mock_client): """Test main() routes gcb-case-close-definition-list command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-close-definition-list") mocker.patch.object(demisto, "args", return_value={}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_close_definition_list_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_close_command_success(mock_client, requests_mock): """Test gcb_case_close_command succeeds for valid case IDs and close reason.""" requests_mock.post(f"{BASE_URL}{ENDPOINTS['CASES_BULK_CLOSE']}", text="", status_code=200) expected_outputs = util_load_json("test_data/case_close_success.json") result = gcb_case_close_command( mock_client, { "case_ids": "1001,1002", "close_reason": "NOT_MALICIOUS", "root_cause": "False positive", "close_comment": "Test comment", }, ) assert result.readable_output == "Cases 1001, 1002 successfully closed with reason NOT_MALICIOUS." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_ids")), ({"case_ids": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("close_reason")), ({"case_ids": "1001", "close_reason": "NOT_MALICIOUS"}, MESSAGES["REQUIRED_ARGUMENT"].format("root_cause")), ( {"case_ids": "abc", "close_reason": "NOT_MALICIOUS", "root_cause": "test"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY), ), ( {"case_ids": "-5", "close_reason": "NOT_MALICIOUS", "root_cause": "test"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "-5", CASE_ID_DISPLAY), ), ( {"case_ids": "0", "close_reason": "NOT_MALICIOUS", "root_cause": "test"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "0", CASE_ID_DISPLAY), ), ( {"case_ids": "1001,abc,1002", "close_reason": "NOT_MALICIOUS", "root_cause": "test"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY), ), ( {"case_ids": "1001", "close_reason": "INVALID_REASON", "root_cause": "test"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("close_reason", ", ".join(VALID_CASE_CLOSE_REASONS)), ), ], ) def test_gcb_case_close_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_close_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_close_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_close_command_success(mocker, mock_client): """Test main() routes gcb-case-close command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-close") mocker.patch.object( demisto, "args", return_value={ "case_ids": "1001", "close_reason": "NOT_MALICIOUS", "root_cause": "False positive", "close_comment": "Test comment", }, ) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_close_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_assign_command_success_email(mock_client, requests_mock): """Test gcb_case_assign_command with an email address resolved to a SOAR user ID.""" user_email = "active.user@example.com" user_id = "00000000-0000-0000-0000-000000000111" args = {"case_ids": "1001, 1002", "assignee": user_email} expected_outputs = [{"caseId": "1001", "assignee": user_id}, {"caseId": "1002", "assignee": user_id}] soar_users_response = util_load_json("test_data/legacy_soar_users_response.json") soar_users_qs = urlencode({"pageSize": 1, "filter": f"(email='{user_email}') AND accountState='ACTIVE'"}) requests_mock.get(f"{BASE_URL}{ENDPOINTS['LEGACY_SOAR_USERS']}?{soar_users_qs}", json=soar_users_response) requests_mock.post(f"{BASE_URL}{ENDPOINTS['CASES_BULK_ASSIGN']}", json={}, status_code=200) result = gcb_case_assign_command(mock_client, args) assert result.readable_output == f"Cases 1001, 1002 successfully assigned to {user_email}." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_assign_command_success_soc_role(mock_client, requests_mock): """Test gcb_case_assign_command with a SOC role passed through unchanged.""" soc_role = "@SOC" args = {"case_ids": "1001, 1002", "assignee": soc_role} expected_outputs = [{"caseId": "1001", "assignee": soc_role}, {"caseId": "1002", "assignee": soc_role}] requests_mock.post(f"{BASE_URL}{ENDPOINTS['CASES_BULK_ASSIGN']}", json={}, status_code=200) result = gcb_case_assign_command(mock_client, args) assert result.readable_output == f"Cases 1001, 1002 successfully assigned to {soc_role}." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs @pytest.mark.parametrize("assignee", [("unknown@example.com"), ("nodisplay.user@example.com")]) def test_gcb_case_assign_command_unresolved_email(mock_client, requests_mock, assignee): """Test gcb_case_assign_command raises ValueError for not found and no-display-name users.""" soar_users_response = util_load_json("test_data/legacy_soar_users_response.json") soar_users_qs = urlencode({"pageSize": 1, "filter": f"(email='{assignee}') AND accountState='ACTIVE'"}) requests_mock.get(f"{BASE_URL}{ENDPOINTS['LEGACY_SOAR_USERS']}?{soar_users_qs}", json=soar_users_response) with pytest.raises(ValueError) as error: gcb_case_assign_command(mock_client, {"case_ids": "1001", "assignee": assignee}) assert str(error.value) == MESSAGES["USER_NOT_FOUND"].format(assignee) def test_gcb_case_assign_command_inactive_user(mock_client, requests_mock): """Test gcb_case_assign_command raises ValueError for inactive users filtered by the API.""" assignee = "inactive.user@example.com" soar_users_qs = urlencode({"pageSize": 1, "filter": f"(email='{assignee}') AND accountState='ACTIVE'"}) requests_mock.get(f"{BASE_URL}{ENDPOINTS['LEGACY_SOAR_USERS']}?{soar_users_qs}", json={"legacySoarUsers": []}) with pytest.raises(ValueError) as error: gcb_case_assign_command(mock_client, {"case_ids": "1001", "assignee": assignee}) assert str(error.value) == MESSAGES["USER_NOT_FOUND"].format(assignee) def test_gcb_case_assign_command_api_error(mock_client, requests_mock): """Test gcb_case_assign_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASES_BULK_ASSIGN']}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403 ) with pytest.raises(ValueError) as error: gcb_case_assign_command(mock_client, {"case_ids": "1001", "assignee": "@role"}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(403, PERMISSION_DENIED_TEXT) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_ids")), ({"case_ids": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("assignee")), ( {"case_ids": "abc", "assignee": "user_email"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY), ), ( {"case_ids": "-5", "assignee": "user_email"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "-5", CASE_ID_DISPLAY), ), ( {"case_ids": "0", "assignee": "user_email"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "0", CASE_ID_DISPLAY), ), ( {"case_ids": "1001,abc", "assignee": "user_email"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_ids", "abc", CASE_ID_DISPLAY), ), ], ) def test_gcb_case_assign_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_assign_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_assign_command(mock_client, args) assert str(error.value) == expected_error_message def test_gcb_case_comment_list_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_comment_list_command should return case comments.""" mock_response = util_load_json("test_data/case_comment_list_success_response.json") outputs = util_load_json("test_data/case_comment_list_context.json") expected_hr = util_load_text_data("test_data/case_comment_list_hr.md") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_COMMENTS'].format(case_id='1001')}?pageSize=50&orderBy=createTime%20desc", json=mock_response, ) result = gcb_case_comment_list_command(mock_client, {"case_id": "1001"}) assert result.outputs == outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_case_comment_list_command_empty_response(mock_client, requests_mock): """When empty response is received, gcb_case_comment_list_command should return appropriate message.""" mock_response = {} requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_COMMENTS'].format(case_id='1001')}?pageSize=50&orderBy=createTime%20desc", json=mock_response, ) result = gcb_case_comment_list_command(mock_client, {"case_id": "1001"}) assert result.raw_response == {} assert "No case comments found" in result.readable_output def test_gcb_case_comment_list_command_with_pagination(mock_client, requests_mock): """Test gcb_case_comment_list_command with pagination parameters.""" mock_response = util_load_json("test_data/case_comment_list_success_response.json") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_COMMENTS'].format(case_id='1001')}?pageSize=10&pageToken=token123&orderBy=updateTime%20asc", json=mock_response, ) result = gcb_case_comment_list_command( mock_client, {"case_id": "1001", "page_size": "10", "page_token": "token123", "sort_by": "updateTime", "sort_order": "Asc"}, ) assert result.raw_response == mock_response assert len(result.outputs) == 2 @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY)), ({"case_id": "-5"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY)), ({"case_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY)), ( {"case_id": "1001", "page_size": "-1"}, MESSAGES["INVALID_INT_RANGE"].format(-1, "page_size", 1, 1000), ), ( {"case_id": "1001", "page_size": "1001"}, MESSAGES["INVALID_INT_RANGE"].format(1001, "page_size", 1, 1000), ), ( {"case_id": "1001", "sort_order": "Invalid"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("sort_order", "Asc, Desc"), ), ( {"case_id": "1001", "sort_order": "ascending"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("sort_order", "Asc, Desc"), ), ], ) def test_gcb_case_comment_list_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_comment_list_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_comment_list_command(mock_client, args) assert str(error.value) == expected_error_message def test_gcb_case_comment_create_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_comment_create_command should return created comment details.""" mock_response = util_load_json("test_data/case_comment_create_success_response.json") outputs = util_load_json("test_data/case_comment_create_context.json") expected_hr = util_load_text_data("test_data/case_comment_create_hr.md") requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_COMMENTS'].format(case_id='1001')}", json=mock_response, ) result = gcb_case_comment_create_command( mock_client, {"case_id": "1001", "comment": "Investigated the outbound traffic. Confirmed malicious C2 communication."} ) assert result.outputs_key_field == "name" assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["CaseComment"] assert result.outputs == outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY)), ({"case_id": "-5"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY)), ({"case_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY)), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("comment")), ({"case_id": "1001", "comment": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("comment")), ], ) def test_gcb_case_comment_create_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_comment_create_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_comment_create_command(mock_client, args) assert str(error.value) == expected_error_message def test_gcb_case_sla_pause_command_success(mock_client, requests_mock): """Test gcb_case_sla_pause_command succeeds for a valid case ID.""" requests_mock.post(f"{BASE_URL}{ENDPOINTS['CASE_SLA_PAUSE'].format(case_id='1001')}", text="", status_code=200) expected_outputs = {"caseId": "1001", "slaStatus": "PAUSED"} result = gcb_case_sla_pause_command( mock_client, {"case_id": "1001", "message": "Pausing SLA pending additional investigation."} ) assert result.readable_output == "SLA timer for case 1001 successfully paused." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_sla_pause_command_success_without_message(mock_client, requests_mock): """Test gcb_case_sla_pause_command succeeds when message is not provided.""" requests_mock.post(f"{BASE_URL}{ENDPOINTS['CASE_SLA_PAUSE'].format(case_id='1001')}", text="", status_code=200) result = gcb_case_sla_pause_command(mock_client, {"case_id": "1001"}) assert result.readable_output == "SLA timer for case 1001 successfully paused." assert result.outputs == {"caseId": "1001", "slaStatus": "PAUSED"} def test_gcb_case_sla_pause_command_api_error(mock_client, requests_mock): """Test gcb_case_sla_pause_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_SLA_PAUSE'].format(case_id='1001')}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_sla_pause_command(mock_client, {"case_id": "1001", "message": "Pausing SLA pending additional investigation."}) assert PERMISSION_DENIED_TEXT in str(error.value) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY)), ({"case_id": "-5"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY)), ({"case_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY)), ], ) def test_gcb_case_sla_pause_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_sla_pause_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_sla_pause_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_sla_pause_command_success(mocker, mock_client): """Test main() routes gcb-case-sla-pause command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-sla-pause") mocker.patch.object( demisto, "args", return_value={"case_id": "1001", "message": "Pausing SLA pending additional investigation."} ) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_sla_pause_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_sla_resume_command_success(mock_client, requests_mock): """Test gcb_case_sla_resume_command succeeds for a valid case ID.""" requests_mock.post(f"{BASE_URL}{ENDPOINTS['CASE_SLA_RESUME'].format(case_id='1001')}", text="", status_code=200) expected_outputs = {"caseId": "1001", "slaStatus": "SLA_EXPIRATION_STATUS_UNSPECIFIED"} result = gcb_case_sla_resume_command(mock_client, {"case_id": "1001"}) assert result.readable_output == "SLA timer for case 1001 successfully resumed." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Case"] assert result.outputs_key_field == "caseId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_sla_resume_command_api_error(mock_client, requests_mock): """Test gcb_case_sla_resume_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_SLA_RESUME'].format(case_id='1001')}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_sla_resume_command(mock_client, {"case_id": "1001"}) assert PERMISSION_DENIED_TEXT in str(error.value) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY)), ({"case_id": "-5"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY)), ({"case_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY)), ], ) def test_gcb_case_sla_resume_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_sla_resume_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_sla_resume_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_sla_resume_command_success(mocker, mock_client): """Test main() routes gcb-case-sla-resume command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-sla-resume") mocker.patch.object(demisto, "args", return_value={"case_id": "1001"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_sla_resume_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_list_command_success(mock_client, requests_mock): """Success with all individual filter args.""" raw_response = util_load_json("test_data/case_alert_list_raw_response.json") expected_context = util_load_json("test_data/case_alert_list_context.json") expected_hr = util_load_text_data("test_data/case_alert_list_hr.md") requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASE_ALERTS'].format(case_id='1001')}", json=raw_response) result = gcb_case_alert_list_command( mock_client, { "case_id": "1001", "priority": "HIGH", "status": "OPEN", "product": "Test Product", "vendor": "Test Vendor", "environment": "Default Environment", "source_system_name": "Test Source System", "tag": "demo_1", "display_name": "TEST ALERT", "manual": "false", "filter_logic": "AND", "sort_by": "priority", "sort_order": "Asc", "create_start_time": "1 week", "create_end_time": "3 days", "update_start_time": "2 weeks", "update_end_time": "1 day", "page_size": "10", }, ) assert result.outputs == expected_context assert result.raw_response == raw_response assert result.readable_output == expected_hr def test_gcb_case_alert_list_command_advanced_filter_success(mock_client, requests_mock): """Success with advanced_filter: individual filters ignored, raw filter used as-is.""" raw_response = util_load_json("test_data/case_alert_list_raw_response.json") expected_context = util_load_json("test_data/case_alert_list_context.json") expected_hr = util_load_text_data("test_data/case_alert_list_hr.md") requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASE_ALERTS'].format(case_id='1001')}", json=raw_response) advanced_filter = "priority='HIGH' AND status='OPEN'" result = gcb_case_alert_list_command( mock_client, { "case_id": "1001", "advanced_filter": advanced_filter, "priority": "LOW", }, ) assert result.raw_response == raw_response assert result.outputs == expected_context assert result.readable_output == expected_hr def test_gcb_case_alert_list_command_empty_response(mock_client, requests_mock): """Empty response with page_token returns no-records message.""" requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASE_ALERTS'].format(case_id='1001')}", json={}) result = gcb_case_alert_list_command(mock_client, {"case_id": "1001", "page_token": "token123", "page_size": "5"}) assert result.raw_response == {} assert MESSAGES["NO_RECORDS_FOUND"].format("case alerts") in result.readable_output @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", "Case ID")), ({"case_id": "-1"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-1", "Case ID")), ( {"case_id": "1001", "page_size": "0"}, MESSAGES["INVALID_INT_RANGE"].format(0, "page_size", 1, MAX_PAGE_SIZE), ), ( {"case_id": "1001", "page_size": "1001"}, MESSAGES["INVALID_INT_RANGE"].format(1001, "page_size", 1, MAX_PAGE_SIZE), ), ( {"case_id": "1001", "sort_order": "INVALID"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("sort_order", ", ".join(VALID_SORT_ORDERS)), ), ( {"case_id": "1001", "priority": "INVALID_PRIORITY"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("priority", ", ".join(VALID_CASE_ALERT_PRIORITIES)), ), ( {"case_id": "1001", "status": "INVALID_STATUS"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("status", ", ".join(VALID_CASE_ALERT_STATUSES)), ), ( {"case_id": "1001", "create_start_time": "2026-05-20", "create_end_time": "2026-05-10"}, MESSAGES["INVALID_DATE_RANGE"].format("create_start_time", "create_end_time"), ), ( {"case_id": "1001", "update_start_time": "2026-06-05T00:00:00Z", "update_end_time": "2026-05-15T00:00:00Z"}, MESSAGES["INVALID_DATE_RANGE"].format("update_start_time", "update_end_time"), ), ( {"case_id": "1001", "filter_logic": "XOR"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("filter_logic", ", ".join(VALID_CASE_FILTER_LOGIC)), ), ], ) def test_gcb_case_alert_list_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_list_command raises ValueError for invalid argument values.""" with pytest.raises((ValueError, SystemExit)) as error: gcb_case_alert_list_command(mock_client, args) assert expected_error_message in str(error.value) def test_gcb_case_alert_get_command_success(mock_client, requests_mock): """Test gcb_case_alert_get_command with a successful API response.""" raw_response = util_load_json("test_data/case_alert_get_raw_response.json") expected_context = util_load_json("test_data/case_alert_get_context.json") expected_hr = util_load_text_data("test_data/case_alert_get_hr.md") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT'].format(case_id='1001', alert_id='1000001')}", json=raw_response, ) result = gcb_case_alert_get_command(mock_client, {"case_id": "1001", "alert_id": "1000001"}) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["CaseAlert"] assert result.outputs_key_field == "alertId" assert result.outputs == expected_context assert result.raw_response == raw_response assert result.readable_output == expected_hr def test_gcb_case_alert_get_command_api_error(mock_client, requests_mock): """Test gcb_case_alert_get_command when API returns a 404 error for a non-existent alert.""" error_message = "Case alert not found" requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT'].format(case_id='1001', alert_id='999999')}", json={"error": {"message": error_message}}, status_code=404, ) with pytest.raises(ValueError) as error: gcb_case_alert_get_command(mock_client, {"case_id": "1001", "alert_id": "999999"}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(404, error_message) def test_gcb_case_alert_get_command_empty_response(mock_client, requests_mock): """Test gcb_case_alert_get_command when API returns empty response.""" requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT'].format(case_id='1001', alert_id='1000001')}", json={}, ) result = gcb_case_alert_get_command(mock_client, {"case_id": "1001", "alert_id": "1000001"}) assert result.readable_output == MESSAGES["NO_RECORDS_FOUND"].format("case alert information") @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "abc", "alert_id": "1000001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ({"case_id": "-1", "alert_id": "1000001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-1", CASE_ID_DISPLAY)), ({"case_id": "0", "alert_id": "1000001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY)), ( {"case_id": "1001", "alert_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ({"case_id": "1001", "alert_id": "-1"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-1", ALERT_ID_DISPLAY)), ({"case_id": "1001", "alert_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "0", ALERT_ID_DISPLAY)), ], ) def test_gcb_case_alert_get_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_get_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_get_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_get_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-get command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-get") mocker.patch.object(demisto, "args", return_value={"case_id": "1001", "alert_id": "1000001"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_get_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) @pytest.mark.parametrize( "args, expected_update_mask_fields", [ ( { "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", }, {"status", "closureDetails", "priority"}, ), ( {"case_id": "1001", "alert_id": "1000001", "priority": "HIGH"}, {"priority"}, ), ( { "case_id": "1001", "alert_id": "1000001", "status": "CLOSE", "close_reason": "MALICIOUS", "root_cause": "Confirmed malicious activity", }, {"status", "closureDetails"}, ), ], ) def test_gcb_case_alert_update_command_success(mock_client, requests_mock, args, expected_update_mask_fields): """Test gcb_case_alert_update_command: full-fields, priority-only, and close-status cases.""" raw_response = util_load_json("test_data/case_alert_update_raw_response.json") expected_context = util_load_json("test_data/case_alert_update_context.json") expected_hr = util_load_text_data("test_data/case_alert_update_hr.md") requests_mock.patch( f"{BASE_URL}{ENDPOINTS['CASE_ALERT'].format(case_id='1001', alert_id='1000001')}", json=raw_response, ) result = gcb_case_alert_update_command(mock_client, args) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["CaseAlert"] assert result.outputs_key_field == "alertId" assert result.outputs == expected_context assert result.raw_response == raw_response assert result.readable_output == expected_hr update_mask_value = requests_mock.last_request.qs["updatemask"][0] assert set(update_mask_value.split(",")) == {f.lower() for f in expected_update_mask_fields} def test_gcb_case_alert_update_command_api_error(mock_client, requests_mock): """Test gcb_case_alert_update_command when API returns a 404 error for a non-existent alert.""" error_message = "Case alert not found" requests_mock.patch( f"{BASE_URL}{ENDPOINTS['CASE_ALERT'].format(case_id='1001', alert_id='999999')}", json={"error": {"message": error_message}}, status_code=404, ) with pytest.raises(ValueError) as error: gcb_case_alert_update_command(mock_client, {"case_id": "1001", "alert_id": "999999", "priority": "HIGH"}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(404, error_message) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "abc", "alert_id": "1000001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-1", "alert_id": "1000001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-1", CASE_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "-1"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-1", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "1000001"}, MESSAGES["AT_LEAST_ONE_REQUIRED"].format(", ".join(CASE_ALERT_UPDATE_ARGS)), ), ( {"case_id": "1001", "alert_id": "1000001", "status": "INVALID"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("status", ", ".join(VALID_CASE_ALERT_STATUSES)), ), ( {"case_id": "1001", "alert_id": "1000001", "priority": "INVALID"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("priority", ", ".join(VALID_CASE_ALERT_PRIORITIES)), ), ( {"case_id": "1001", "alert_id": "1000001", "status": "CLOSE"}, "close_reason is required when status is CLOSE.", ), ( {"case_id": "1001", "alert_id": "1000001", "status": "CLOSE", "close_reason": "MALICIOUS"}, "root_cause is required when status is CLOSE.", ), ( {"case_id": "1001", "alert_id": "1000001", "status": "CLOSE", "close_reason": "INVALID", "root_cause": "some cause"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("close_reason", ", ".join(VALID_CASE_ALERT_CLOSE_REASONS)), ), ], ) def test_gcb_case_alert_update_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_update_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_update_command(mock_client, args) assert str(error.value) == expected_error_message def test_gcb_case_alert_tag_add_command_success(mock_client, requests_mock): """Test gcb_case_alert_tag_add_command with a successful response.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_ADD_TAG'].format(case_id='1001', alert_id='1142656')}", text="", status_code=200, ) expected_outputs = {"alertId": "1142656", "caseId": 1001, "recentlyAddedTag": "insider-threat"} result = gcb_case_alert_tag_add_command(mock_client, {"case_id": "1001", "alert_id": "1142656", "tag": "insider-threat"}) assert result.readable_output == "Tag insider-threat successfully added to alert 1142656." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["CaseAlert"] assert result.outputs_key_field == "alertId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_alert_tag_add_command_api_error(mock_client, requests_mock): """Test gcb_case_alert_tag_add_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_ADD_TAG'].format(case_id='1001', alert_id='1142656')}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_alert_tag_add_command(mock_client, {"case_id": "1001", "alert_id": "1142656", "tag": "insider-threat"}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(403, PERMISSION_DENIED_TEXT) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": "1142656"}, MESSAGES["REQUIRED_ARGUMENT"].format("tag")), ( {"case_id": "abc", "alert_id": "1142656", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1142656", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ( {"case_id": "0", "alert_id": "1142656", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "abc", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "-5", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "0", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "0", ALERT_ID_DISPLAY), ), ], ) def test_gcb_case_alert_tag_add_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_tag_add_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_tag_add_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_tag_add_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-tag-add command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-tag-add") mocker.patch.object(demisto, "args", return_value={"case_id": "1001", "alert_id": "1142656", "tag": "insider-threat"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_tag_add_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_tag_remove_command_success(mock_client, requests_mock): """Test gcb_case_alert_tag_remove_command with a successful response.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_REMOVE_TAG'].format(case_id='1001', alert_id='1142656')}", text="", status_code=200, ) expected_outputs = {"alertId": "1142656", "caseId": 1001, "recentlyRemovedTag": "insider-threat"} result = gcb_case_alert_tag_remove_command(mock_client, {"case_id": "1001", "alert_id": "1142656", "tag": "insider-threat"}) assert result.readable_output == "Tag insider-threat successfully removed from alert 1142656." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["CaseAlert"] assert result.outputs_key_field == "alertId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_alert_tag_remove_command_api_error(mock_client, requests_mock): """Test gcb_case_alert_tag_remove_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_REMOVE_TAG'].format(case_id='1001', alert_id='1142656')}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_alert_tag_remove_command(mock_client, {"case_id": "1001", "alert_id": "1142656", "tag": "insider-threat"}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(403, PERMISSION_DENIED_TEXT) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": "1142656"}, MESSAGES["REQUIRED_ARGUMENT"].format("tag")), ( {"case_id": "abc", "alert_id": "1142656", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1142656", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ( {"case_id": "0", "alert_id": "1142656", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "abc", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "-5", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "0", "tag": "malware"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "0", ALERT_ID_DISPLAY), ), ], ) def test_gcb_case_alert_tag_remove_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_tag_remove_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_tag_remove_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_tag_remove_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-tag-remove command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-tag-remove") mocker.patch.object(demisto, "args", return_value={"case_id": "1001", "alert_id": "1142656", "tag": "insider-threat"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_tag_remove_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_move_command_success(mock_client, requests_mock): """Test gcb_case_alert_move_command with a successful response.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_MOVE'].format(case_id='1001', alert_id='1142656')}", json={"newCaseId": 1005, "valid": True}, status_code=200, ) result = gcb_case_alert_move_command(mock_client, {"case_id": "1001", "alert_id": "1142656", "destination_case_id": "1005"}) assert result.readable_output == "Successfully moved Alert `1142656` to Case `1005`." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["CaseAlert"] assert result.outputs_key_field == "alertId" assert result.outputs == {"alertId": "1142656", "caseId": 1005} assert result.raw_response == {"newCaseId": 1005, "valid": True} def test_gcb_case_alert_move_command_api_error(mock_client, requests_mock): """Test gcb_case_alert_move_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_MOVE'].format(case_id='1001', alert_id='1142656')}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_alert_move_command(mock_client, {"case_id": "1001", "alert_id": "1142656", "destination_case_id": "1005"}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(403, PERMISSION_DENIED_TEXT) def test_gcb_case_alert_move_command_errors_in_response(mock_client, requests_mock): """Test gcb_case_alert_move_command when API response contains errors array.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_MOVE'].format(case_id='1001', alert_id='1142656')}", json={"newCaseId": 1005, "errors": ["Cannot move alert to a new case to a different environment"]}, status_code=200, ) with pytest.raises(Exception) as error: gcb_case_alert_move_command(mock_client, {"case_id": "1001", "alert_id": "1142656", "destination_case_id": "1005"}) assert "Cannot move alert to a new case to a different environment" in str(error.value) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": "1142656"}, MESSAGES["REQUIRED_ARGUMENT"].format("destination_case_id")), ( {"case_id": "abc", "alert_id": "1142656", "destination_case_id": "1005"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1142656", "destination_case_id": "1005"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ( {"case_id": "0", "alert_id": "1142656", "destination_case_id": "1005"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "abc", "destination_case_id": "1005"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "-5", "destination_case_id": "1005"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "0", "destination_case_id": "1005"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "0", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "1142656", "destination_case_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("destination_case_id", "abc", "Destination Case ID"), ), ( {"case_id": "1001", "alert_id": "1142656", "destination_case_id": "-5"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("destination_case_id", "-5", "Destination Case ID"), ), ( {"case_id": "1001", "alert_id": "1142656", "destination_case_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("destination_case_id", "0", "Destination Case ID"), ), ], ) def test_gcb_case_alert_move_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_move_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_move_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_move_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-move command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-move") mocker.patch.object(demisto, "args", return_value={"case_id": "1001", "alert_id": "1142656", "destination_case_id": "1005"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_move_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) # --- gcb-case-alert-sla-pause --- def test_gcb_case_alert_sla_pause_command_success(mock_client, requests_mock): """Test gcb_case_alert_sla_pause_command succeeds with a message.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_SLA_PAUSE'].format(case_id='1001', alert_id='1142656')}", text="", status_code=200, ) result = gcb_case_alert_sla_pause_command( mock_client, {"case_id": "1001", "alert_id": "1142656", "message": "Pausing SLA pending additional investigation."}, ) assert result.readable_output == "SLA timer for alert 1142656 successfully paused." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["CaseAlert"] assert result.outputs_key_field == "alertId" assert result.outputs == {"alertId": "1142656", "caseId": 1001, "slaExpirationStatus": "PAUSED"} assert result.raw_response == result.outputs def test_gcb_case_alert_sla_pause_command_success_without_message(mock_client, requests_mock): """Test gcb_case_alert_sla_pause_command succeeds when message is not provided.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_SLA_PAUSE'].format(case_id='1001', alert_id='1142656')}", text="", status_code=200, ) result = gcb_case_alert_sla_pause_command(mock_client, {"case_id": "1001", "alert_id": "1142656"}) assert result.readable_output == "SLA timer for alert 1142656 successfully paused." assert result.outputs == {"alertId": "1142656", "caseId": 1001, "slaExpirationStatus": "PAUSED"} def test_gcb_case_alert_sla_pause_command_api_error(mock_client, requests_mock): """Test gcb_case_alert_sla_pause_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_SLA_PAUSE'].format(case_id='1001', alert_id='1142656')}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_alert_sla_pause_command(mock_client, {"case_id": "1001", "alert_id": "1142656"}) assert PERMISSION_DENIED_TEXT in str(error.value) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "abc", "alert_id": "1142656"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1142656"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ( {"case_id": "0", "alert_id": "1142656"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "-5"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "0", ALERT_ID_DISPLAY), ), ], ) def test_gcb_case_alert_sla_pause_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_sla_pause_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_sla_pause_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_sla_pause_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-sla-pause command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-sla-pause") mocker.patch.object(demisto, "args", return_value={"case_id": "1001", "alert_id": "1142656", "message": "Pausing SLA."}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_sla_pause_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) # --- gcb-case-alert-sla-resume --- def test_gcb_case_alert_sla_resume_command_success(mock_client, requests_mock): """Test gcb_case_alert_sla_resume_command succeeds for valid case and alert IDs.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_SLA_RESUME'].format(case_id='1001', alert_id='1142656')}", text="", status_code=200, ) expected_outputs = {"alertId": "1142656", "caseId": 1001, "slaExpirationStatus": "SLA_EXPIRATION_STATUS_UNSPECIFIED"} result = gcb_case_alert_sla_resume_command(mock_client, {"case_id": "1001", "alert_id": "1142656"}) assert result.readable_output == "SLA timer for alert 1142656 successfully resumed." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["CaseAlert"] assert result.outputs_key_field == "alertId" assert result.outputs == expected_outputs assert result.raw_response == expected_outputs def test_gcb_case_alert_sla_resume_command_api_error(mock_client, requests_mock): """Test gcb_case_alert_sla_resume_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_SLA_RESUME'].format(case_id='1001', alert_id='1142656')}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_alert_sla_resume_command(mock_client, {"case_id": "1001", "alert_id": "1142656"}) assert PERMISSION_DENIED_TEXT in str(error.value) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "abc", "alert_id": "1142656"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1142656"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ( {"case_id": "0", "alert_id": "1142656"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "-5"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "0", ALERT_ID_DISPLAY), ), ], ) def test_gcb_case_alert_sla_resume_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_sla_resume_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_sla_resume_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_sla_resume_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-sla-resume command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-sla-resume") mocker.patch.object(demisto, "args", return_value={"case_id": "1001", "alert_id": "1142656"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_sla_resume_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) # --- gcb-case-alert-sla-set --- @pytest.mark.parametrize( "time_str, expected_ms, tolerance_ms", [ ("2 minutes", 2 * 60 * 1000, 1000), ("1 hour", 60 * 60 * 1000, 1000), ("3 days", 3 * TEST_DAY_MS, 1000), ("2 weeks", 14 * TEST_DAY_MS, 1000), ("1 month", 30 * TEST_DAY_MS, 2 * TEST_DAY_MS), ("2 years", 2 * 365 * TEST_DAY_MS, 2 * TEST_DAY_MS), ], ) def test_convert_time_to_ms_relative_durations(time_str, expected_ms, tolerance_ms): """Test convert_time_to_ms with relative duration strings — within tolerance for calendar variation.""" result = convert_time_to_ms(time_str, "test") assert abs(result - expected_ms) <= tolerance_ms @pytest.mark.parametrize( "time_str, match", [ ("not-a-date-at-all-xyz", "Invalid date"), ("2000-01-01", "is in the past"), ], ) def test_convert_time_to_ms_invalid_input_raises(time_str, match): """Test convert_time_to_ms raises ValueError for unparseable or past-date input.""" with pytest.raises(ValueError, match=match): convert_time_to_ms(time_str, "test_arg") @pytest.mark.parametrize( "case_id, alert_id, total_time, critical_time, expected_error", [ ("abc", "1142656", "2 days", None, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY)), ("-1", "1142656", "2 days", None, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-1", CASE_ID_DISPLAY)), ("1001", "abc", "2 days", None, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY)), ("1001", "0", "2 days", None, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "0", ALERT_ID_DISPLAY)), ("1001", "1142656", "1 day", "2 days", "total_time must be greater than critical_time"), ( "1001", "1142656", "18-5-2026", "2 weeks", MESSAGES["TIME_IN_PAST"].format(arg_name="total_time", time_str="18-5-2026"), ), ( "1001", "1142656", "2 weeks", "18-5-2026", MESSAGES["TIME_IN_PAST"].format(arg_name="critical_time", time_str="18-5-2026"), ), ], ) def test_validate_case_alert_sla_set_args_invalid(case_id, alert_id, total_time, critical_time, expected_error): """Test validate_case_alert_sla_set_args raises ValueError for invalid inputs.""" with pytest.raises(ValueError, match=expected_error): validate_case_alert_sla_set_args(case_id, alert_id, total_time, critical_time) def test_validate_case_alert_sla_set_args_valid(): """Test validate_case_alert_sla_set_args returns correct values for valid inputs.""" two_days_ms = 2 * 24 * 60 * 60 * 1000 one_day_ms = 24 * 60 * 60 * 1000 total_ms, critical_ms = validate_case_alert_sla_set_args("1001", "1142656", "2 days", "1 day") assert abs(total_ms - two_days_ms) <= 1000 assert critical_ms is not None assert abs(critical_ms - one_day_ms) <= 1000 def test_validate_case_alert_sla_set_args_without_critical_time(): """Test validate_case_alert_sla_set_args returns None for critical_time_ms when not provided.""" total_ms, critical_ms = validate_case_alert_sla_set_args("1001", "1142656", "2 days", None) assert abs(total_ms - 2 * 24 * 60 * 60 * 1000) <= 1000 assert critical_ms is None def test_gcb_case_alert_sla_set_command_success(mock_client, requests_mock): """Test gcb_case_alert_sla_set_command succeeds with valid inputs.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_SLA_SET'].format(case_id='1001', alert_id='1142656')}", text="", status_code=200, ) result = gcb_case_alert_sla_set_command( mock_client, {"case_id": "1001", "alert_id": "1142656", "total_time": "2 days", "critical_time": "1 day"}, ) assert result.readable_output == "SLA for Alert `1142656` successfully set." assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["CaseAlert"] assert result.outputs_key_field == "alertId" assert result.outputs["alertId"] == "1142656" assert result.outputs["caseId"] == 1001 assert isinstance(result.outputs["slaExpirationTime"], str) assert isinstance(result.outputs["slaCriticalExpirationTime"], str) assert result.raw_response == result.outputs def test_gcb_case_alert_sla_set_command_success_without_critical_time(mock_client, requests_mock): """Test gcb_case_alert_sla_set_command succeeds without critical_time.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_SLA_SET'].format(case_id='1001', alert_id='1142656')}", text="", status_code=200, ) result = gcb_case_alert_sla_set_command( mock_client, {"case_id": "1001", "alert_id": "1142656", "total_time": "2 days"}, ) assert result.readable_output == "SLA for Alert `1142656` successfully set." assert result.outputs["alertId"] == "1142656" assert result.outputs["caseId"] == 1001 assert isinstance(result.outputs["slaExpirationTime"], str) assert "slaCriticalExpirationTime" not in result.outputs def test_gcb_case_alert_sla_set_command_api_error(mock_client, requests_mock): """Test gcb_case_alert_sla_set_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_SLA_SET'].format(case_id='1001', alert_id='1142656')}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_alert_sla_set_command( mock_client, {"case_id": "1001", "alert_id": "1142656", "total_time": "2 days", "critical_time": "1 day"}, ) assert PERMISSION_DENIED_TEXT in str(error.value) def test_gcb_case_alert_sla_set_command_total_not_greater_than_critical(mock_client): """Test gcb_case_alert_sla_set_command raises when total_time <= critical_time.""" with pytest.raises(ValueError, match="total_time must be greater than critical_time"): gcb_case_alert_sla_set_command( mock_client, {"case_id": "1001", "alert_id": "1142656", "total_time": "1 day", "critical_time": "2 days"}, ) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": "1142656"}, MESSAGES["REQUIRED_ARGUMENT"].format("total_time")), ( {"case_id": "1001", "alert_id": "1142656", "total_time": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("total_time"), ), ( {"case_id": "abc", "alert_id": "1142656", "total_time": "2 days"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "abc", "total_time": "2 days"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ], ) def test_gcb_case_alert_sla_set_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_sla_set_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_sla_set_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_sla_set_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-sla-set command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-sla-set") mocker.patch.object( demisto, "args", return_value={"case_id": "1001", "alert_id": "1142656", "total_time": "2 days", "critical_time": "1 day"} ) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_sla_set_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_recommendation_create_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_alert_recommendation_create_command should return recommendation details.""" mock_response = {"recommendationId": "00000000-0000-0000-0000-000000000001"} requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_RECOMMENDATION_CREATE'].format(case_id='1001', alert_id='1000001')}", json=mock_response, ) expected_readable_output = ( "Successfully created the recommendation for the alert 1000001.\n\n" "Recommendation ID: 00000000-0000-0000-0000-000000000001" ) result = gcb_case_alert_recommendation_create_command(mock_client, {"case_id": "1001", "alert_id": "1000001"}) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["AlertRecommendation"] assert result.outputs_key_field == "recommendationId" assert result.outputs == mock_response assert result.raw_response == mock_response assert result.readable_output == expected_readable_output def test_gcb_case_alert_recommendation_create_command_api_error(mock_client, requests_mock): """Test gcb_case_alert_recommendation_create_command when API returns a permission denied error.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_RECOMMENDATION_CREATE'].format(case_id='1001', alert_id='1000001')}", json={"error": {"message": PERMISSION_DENIED_TEXT}}, status_code=403, ) with pytest.raises(ValueError) as error: gcb_case_alert_recommendation_create_command(mock_client, {"case_id": "1001", "alert_id": "1000001"}) assert PERMISSION_DENIED_TEXT in str(error.value) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "abc", "alert_id": "1000001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ({"case_id": "-5", "alert_id": "1000001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY)), ({"case_id": "0", "alert_id": "1000001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY)), ( {"case_id": "1001", "alert_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ({"case_id": "1001", "alert_id": "-5"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY)), ({"case_id": "1001", "alert_id": "0"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "0", ALERT_ID_DISPLAY)), ], ) def test_gcb_case_alert_recommendation_create_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_recommendation_create_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_recommendation_create_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_recommendation_create_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-recommendation-create command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-recommendation-create") mocker.patch.object(demisto, "args", return_value={"case_id": "1001", "alert_id": "1000001"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_recommendation_create_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_recommendation_fetch_command_success(mock_client, requests_mock): """Test gcb_case_alert_recommendation_fetch_command with a successful SUCCEEDED response.""" mock_response = util_load_json("test_data/case_alert_recommendation_fetch_response.json") expected_outputs = util_load_json("test_data/case_alert_recommendation_fetch_context.json") expected_hr = util_load_text_data("test_data/case_alert_recommendation_fetch_hr.md") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_FETCH_RECOMMENDATION'].format(case_id='1001')}", json=mock_response, ) result = gcb_case_alert_recommendation_fetch_command( mock_client, {"case_id": "1001", "recommendation_id": "00000000-0000-0000-0000-000000001000"} ) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["AlertRecommendation"] assert result.outputs_key_field == "recommendationId" assert result.outputs == expected_outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_case_alert_recommendation_fetch_command_empty_response(mock_client, requests_mock): """Test gcb_case_alert_recommendation_fetch_command when API returns an empty response.""" requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASE_ALERT_FETCH_RECOMMENDATION'].format(case_id='1001')}", json={}) result = gcb_case_alert_recommendation_fetch_command( mock_client, {"case_id": "1001", "recommendation_id": "00000000-0000-0000-0000-000000001000"} ) assert result.readable_output == MESSAGES["NO_RECORDS_FOUND"].format("alert recommendation") def test_gcb_case_alert_recommendation_fetch_command_api_error(mock_client, requests_mock): """Test gcb_case_alert_recommendation_fetch_command when API returns a 404 for a non-existent recommendation.""" error_message = "Recommendation not found" requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_FETCH_RECOMMENDATION'].format(case_id='1001')}", json={"error": {"message": error_message}}, status_code=404, ) with pytest.raises(ValueError) as error: gcb_case_alert_recommendation_fetch_command(mock_client, {"case_id": "1001", "recommendation_id": "rec-nonexistent"}) assert str(error.value) == MESSAGES["HTTP_ERROR"].format(404, error_message) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("recommendation_id")), ({"case_id": "1001", "recommendation_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("recommendation_id")), ( {"case_id": "abc", "recommendation_id": "00000000-0000-0000-0000-000000001000"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "recommendation_id": "00000000-0000-0000-0000-000000001000"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ( {"case_id": "0", "recommendation_id": "00000000-0000-0000-0000-000000001000"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "0", CASE_ID_DISPLAY), ), ], ) def test_gcb_case_alert_recommendation_fetch_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_recommendation_fetch_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_recommendation_fetch_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_recommendation_fetch_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-recommendation-fetch command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-recommendation-fetch") mocker.patch.object( demisto, "args", return_value={"case_id": "1001", "recommendation_id": "00000000-0000-0000-0000-000000001000"} ) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_recommendation_fetch_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_customfield_list_command_success(mock_client, requests_mock): """When a valid response is received, gcb_case_alert_customfield_list_command should return custom field values.""" mock_response = util_load_json("test_data/case_alert_customfield_list_success_response.json") expected_outputs = util_load_json("test_data/case_alert_customfield_list_context.json") expected_hr = util_load_text_data("test_data/case_alert_customfield_list_hr.md") mock_custom_fields = util_load_json("test_data/case_alert_customfield_list_custom_fields_response.json") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_CUSTOMFIELD_VALUES'].format(case_id='1001', alert_id='2001')}", json=mock_response ) requests_mock.get( f"{BASE_URL}{ENDPOINTS['CUSTOM_FIELDS']}", json=mock_custom_fields, ) result = gcb_case_alert_customfield_list_command(mock_client, {"case_id": "1001", "alert_id": "2001"}) assert result.outputs == expected_outputs assert result.readable_output == expected_hr assert result.raw_response == mock_response def test_gcb_case_alert_customfield_list_command_empty_response(mock_client, requests_mock): """When no custom field values are found, gcb_case_alert_customfield_list_command should return appropriate message.""" requests_mock.get(f"{BASE_URL}{ENDPOINTS['CASE_ALERT_CUSTOMFIELD_VALUES'].format(case_id='1001', alert_id='2001')}", json={}) result = gcb_case_alert_customfield_list_command(mock_client, {"case_id": "1001", "alert_id": "2001"}) assert result.outputs is None assert result.readable_output == MESSAGES["NO_RECORDS_FOUND"].format("custom field values") @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "1001", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "abc", "alert_id": "2001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-1", "alert_id": "2001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-1", CASE_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "-1"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-1", ALERT_ID_DISPLAY), ), ( {"case_id": "1001", "alert_id": "2001", "page_size": "0"}, MESSAGES["INVALID_INT_RANGE"].format(0, "page_size", 1, MAX_PAGE_SIZE), ), ( {"case_id": "1001", "alert_id": "2001", "page_size": "1001"}, MESSAGES["INVALID_INT_RANGE"].format(1001, "page_size", 1, MAX_PAGE_SIZE), ), ], ) def test_gcb_case_alert_customfield_list_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_customfield_list_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_customfield_list_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_customfield_list_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-customfield-list command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-customfield-list") mocker.patch.object(demisto, "args", return_value={"case_id": "1001", "alert_id": "2001"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_customfield_list_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_customfield_list_command_list_custom_fields_failure(mock_client, requests_mock): """When list_custom_fields raises an exception, the command should not hard-fail but return results with empty display names.""" mock_response = util_load_json("test_data/case_alert_customfield_list_success_response.json") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_CUSTOMFIELD_VALUES'].format(case_id='1001', alert_id='2001')}", json=mock_response ) requests_mock.get( f"{BASE_URL}{ENDPOINTS['CUSTOM_FIELDS']}", exc=Exception("network error"), ) with pytest.raises(Exception, match="network error"): gcb_case_alert_customfield_list_command(mock_client, {"case_id": "1001", "alert_id": "2001"}) def test_gcb_case_alert_customfield_list_command_empty_display_name_fallback(mock_client, requests_mock): """When list_custom_fields returns no matching fields, display names should fall back to empty string.""" mock_response = util_load_json("test_data/case_alert_customfield_list_success_response.json") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_CUSTOMFIELD_VALUES'].format(case_id='1001', alert_id='2001')}", json=mock_response ) requests_mock.get( f"{BASE_URL}{ENDPOINTS['CUSTOM_FIELDS']}", json={"customFields": []}, ) result = gcb_case_alert_customfield_list_command(mock_client, {"case_id": "1001", "alert_id": "2001"}) cfv_key = next(k for k in result.outputs if "AlertCustomFieldValue" in k) for entry in result.outputs[cfv_key]: assert entry.get("displayName", "") == "" def test_gcb_case_alert_entity_list_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_alert_entity_list_command should return alert entities.""" mock_response = util_load_json("test_data/case_alert_entity_list_success_response.json") outputs = util_load_json("test_data/case_alert_entity_list_context.json") expected_hr = util_load_text_data("test_data/case_alert_entity_list_hr.md") requests_mock.get( ( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITIES'].format(case_id='306082', alert_id='1001')}" "?pageSize=50&orderBy=id%20desc" ), json=mock_response, ) result = gcb_case_alert_entity_list_command(mock_client, {"case_id": "306082", "alert_id": "1001"}) assert result.outputs == outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_case_alert_entity_list_command_empty_response(mock_client, requests_mock): """When empty response is received, gcb_case_alert_entity_list_command should return appropriate message.""" mock_response = {} requests_mock.get( ( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITIES'].format(case_id='306082', alert_id='1001')}" "?pageSize=50&orderBy=id%20desc" ), json=mock_response, ) result = gcb_case_alert_entity_list_command(mock_client, {"case_id": "306082", "alert_id": "1001"}) assert result.raw_response == {} assert "No alert entities found" in result.readable_output def test_gcb_case_alert_entity_list_command_with_pagination(mock_client, requests_mock): """Test gcb_case_alert_entity_list_command with pagination parameters and next page token in response.""" mock_response = util_load_json("test_data/case_alert_entity_list_paginated_response.json") requests_mock.get( ( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITIES'].format(case_id='306082', alert_id='1001')}" "?pageSize=1&pageToken=prev-token&orderBy=id%20asc" ), json=mock_response, ) result = gcb_case_alert_entity_list_command( mock_client, {"case_id": "306082", "alert_id": "1001", "page_size": "1", "page_token": "prev-token", "sort_order": "Asc"}, ) assert result.raw_response == mock_response assert len(result.outputs) == 2 assert "mock-entity-page-token" in result.readable_output def test_gcb_case_alert_entity_list_command_with_advanced_filter(mock_client, requests_mock): """Test gcb_case_alert_entity_list_command passes advanced_filter directly to API.""" mock_response = util_load_json("test_data/case_alert_entity_list_success_response.json") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITIES'].format(case_id='306082', alert_id='1001')}", json=mock_response, ) result = gcb_case_alert_entity_list_command( mock_client, {"case_id": "306082", "alert_id": "1001", "advanced_filter": "entityType='ADDRESS' AND suspicious=true"}, ) assert result.raw_response == mock_response assert "filter=entityType" in requests_mock.last_request.url def test_gcb_case_alert_entity_list_command_with_filter_logic_or(mock_client, requests_mock): """Test gcb_case_alert_entity_list_command uses OR separator when filter_logic=OR.""" mock_response = util_load_json("test_data/case_alert_entity_list_success_response.json") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITIES'].format(case_id='306082', alert_id='1001')}", json=mock_response, ) result = gcb_case_alert_entity_list_command( mock_client, {"case_id": "306082", "alert_id": "1001", "suspicious": "true", "internal": "false", "filter_logic": "OR"}, ) assert result.raw_response == mock_response assert "suspicious%3Dtrue+OR+internal%3Dfalse" in requests_mock.last_request.url or "OR" in requests_mock.last_request.url @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ( {"case_id": "abc", "alert_id": "1001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1001"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ({"case_id": "306082"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "306082", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "306082", "alert_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "1001", "page_size": "-1"}, MESSAGES["INVALID_INT_RANGE"].format(-1, "page_size", 1, 1000), ), ( {"case_id": "306082", "alert_id": "1001", "page_size": "1001"}, MESSAGES["INVALID_INT_RANGE"].format(1001, "page_size", 1, 1000), ), ( {"case_id": "306082", "alert_id": "1001", "sort_order": "Invalid"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("sort_order", "Asc, Desc"), ), ( {"case_id": "306082", "alert_id": "1001", "filter_logic": "XOR"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("filter_logic", ", ".join(VALID_CASE_FILTER_LOGIC)), ), ], ) def test_gcb_case_alert_entity_list_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_entity_list_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_entity_list_command(mock_client, args) assert str(error.value) == expected_error_message @pytest.mark.parametrize( "case_id, alert_id, page_size, sort_order, filter_logic", [ ("306082", "1001", 50, "Desc", "AND"), ("306082", "1001", 1, "Asc", "OR"), ("306082", "1001", 1000, "Desc", "AND"), ], ) def test_validate_case_alert_entity_list_args_valid(case_id, alert_id, page_size, sort_order, filter_logic): """Test validate_case_alert_entity_list_args with valid inputs.""" result = validate_case_alert_entity_list_args(case_id, alert_id, page_size, sort_order, filter_logic) assert result == (case_id, alert_id, page_size, sort_order, filter_logic) def test_main_gcb_case_alert_entity_list_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-entity-list command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-entity-list") mocker.patch.object(demisto, "args", return_value={"case_id": "306082", "alert_id": "1001"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_entity_list_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) @pytest.mark.parametrize( "kwargs, expected", [ ( {"entity_types": ["ADDRESS", "HOSTNAME"]}, '(type="ADDRESS" OR type="HOSTNAME")', ), ( {"suspicious": True, "internal": False}, "suspicious=true AND internal=false", ), ( {"threat_sources": ["malware"], "environments": ["Production", "Staging"]}, '(threatSource="malware") AND (environment="Production" OR environment="Staging")', ), ( {"entity_types": ["ADDRESS"], "suspicious": True, "environments": ["Default Environment"]}, '(type="ADDRESS") AND suspicious=true AND (environment="Default Environment")', ), ( {"network_priorities": ["0", "1"]}, "(networkPriority=0 OR networkPriority=1)", ), ({}, ""), ], ) def test_prepare_alert_entity_filter(kwargs, expected): """Test prepare_alert_entity_filter builds correct AIP-160 filter strings.""" assert prepare_alert_entity_filter(**kwargs) == expected def test_gcb_case_alert_entity_get_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_alert_entity_get_command should return entity details.""" mock_response = util_load_json("test_data/case_alert_entity_get_success_response.json") outputs = util_load_json("test_data/case_alert_entity_get_context.json") expected_hr = util_load_text_data("test_data/case_alert_entity_get_hr.md") requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITY'].format(case_id='306082', alert_id='1001', entity_id='376359')}", json=mock_response, ) result = gcb_case_alert_entity_get_command(mock_client, {"case_id": "306082", "alert_id": "1001", "entity_id": "376359"}) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["AlertEntity"] assert result.outputs_key_field == "id" assert result.outputs == outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_case_alert_entity_get_command_empty_response(mock_client, requests_mock): """When empty response is received, gcb_case_alert_entity_get_command should return appropriate message.""" mock_response = {} requests_mock.get( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITY'].format(case_id='306082', alert_id='1001', entity_id='376359')}", json=mock_response, ) result = gcb_case_alert_entity_get_command(mock_client, {"case_id": "306082", "alert_id": "1001", "entity_id": "376359"}) assert result.raw_response == {} assert result.outputs is None assert "No entity information found" in result.readable_output @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ( {"case_id": "abc", "alert_id": "1001", "entity_id": "376359"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1001", "entity_id": "376359"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "-5", "entity_id": "376359"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY), ), ({"case_id": "306082"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "306082", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "306082", "alert_id": "abc", "entity_id": "376359"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ({"case_id": "306082", "alert_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("entity_id")), ({"case_id": "306082", "alert_id": "1001", "entity_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("entity_id")), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "abc"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", "abc", ENTITY_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "-1"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", "-1", ENTITY_ID_DISPLAY), ), ], ) def test_gcb_case_alert_entity_get_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_entity_get_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_entity_get_command(mock_client, args) assert str(error.value) == expected_error_message @pytest.mark.parametrize( "case_id, alert_id, entity_id", [ ("306082", "1001", "376359"), ("1", "1", "1"), ], ) def test_validate_case_alert_entity_get_args_valid(case_id, alert_id, entity_id): """Test validate_case_alert_entity_get_args does not raise for valid inputs.""" validate_case_alert_entity_get_args(case_id, alert_id, entity_id) def test_main_gcb_case_alert_entity_get_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-entity-get command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-entity-get") mocker.patch.object(demisto, "args", return_value={"case_id": "306082", "alert_id": "1001", "entity_id": "376359"}) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_entity_get_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_entity_create_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_alert_entity_create_command should return created entity details.""" mock_response = util_load_json("test_data/case_alert_entity_create_success_response.json") outputs = util_load_json("test_data/case_alert_entity_create_context.json") expected_hr = util_load_text_data("test_data/case_alert_entity_create_hr.md") requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITIES'].format(case_id='306082', alert_id='1001')}", json=mock_response, ) result = gcb_case_alert_entity_create_command( mock_client, {"case_id": "306082", "alert_id": "1001", "identifier": MY_IP, "entity_type": "ADDRESS", "suspicious": "true"}, ) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["AlertEntity"] assert result.outputs_key_field == "id" assert result.outputs == outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_case_alert_entity_create_command_network_priority(mock_client, requests_mock): """When network_priority is provided, it should be sent in the request body.""" mock_response = util_load_json("test_data/case_alert_entity_create_success_response.json") post_mock = requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITIES'].format(case_id='306082', alert_id='1001')}", json=mock_response, ) gcb_case_alert_entity_create_command( mock_client, { "case_id": "306082", "alert_id": "1001", "identifier": MY_IP, "entity_type": "ADDRESS", "network_priority": "5", }, ) assert post_mock.last_request.json().get("networkPriority") == 5 def test_gcb_case_alert_entity_create_command_empty_response(mock_client, requests_mock): """When empty response is received, gcb_case_alert_entity_create_command should return appropriate message.""" requests_mock.post( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITIES'].format(case_id='306082', alert_id='1001')}", json={}, ) result = gcb_case_alert_entity_create_command( mock_client, {"case_id": "306082", "alert_id": "1001", "identifier": MY_IP, "entity_type": "ADDRESS"} ) assert result.raw_response == {} assert result.outputs is None assert "No entity information found" in result.readable_output @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ( {"case_id": "abc", "alert_id": "1001", "identifier": "test", "entity_type": "ADDRESS"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1001", "identifier": "test", "entity_type": "ADDRESS"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ({"case_id": "306082"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "306082", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "306082", "alert_id": "abc", "identifier": "test", "entity_type": "ADDRESS"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "-5", "identifier": "test", "entity_type": "ADDRESS"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY), ), ({"case_id": "306082", "alert_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("identifier")), ({"case_id": "306082", "alert_id": "1001", "identifier": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("identifier")), ( {"case_id": "306082", "alert_id": "1001", "identifier": "test"}, MESSAGES["REQUIRED_ARGUMENT"].format("entity_type"), ), ( {"case_id": "306082", "alert_id": "1001", "identifier": "test", "entity_type": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("entity_type"), ), ( { "case_id": "306082", "alert_id": "1001", "identifier": "test", "entity_type": "ADDRESS", "network_priority": "-1", }, MESSAGES["INVALID_NON_NEGATIVE_INTEGER"].format(-1, "network_priority"), ), ], ) def test_gcb_case_alert_entity_create_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_entity_create_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_entity_create_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_entity_create_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-entity-create command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-entity-create") mocker.patch.object( demisto, "args", return_value={"case_id": "306082", "alert_id": "1001", "identifier": MY_IP, "entity_type": "ADDRESS"}, ) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_entity_create_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_entity_update_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_alert_entity_update_command should return updated entity details.""" mock_response = util_load_json("test_data/case_alert_entity_update_success_response.json") outputs = util_load_json("test_data/case_alert_entity_update_context.json") expected_hr = util_load_text_data("test_data/case_alert_entity_update_hr.md") requests_mock.patch( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITY'].format(case_id='306082', alert_id='1001', entity_id='376398')}", json=mock_response, ) result = gcb_case_alert_entity_update_command( mock_client, { "case_id": "306082", "alert_id": "1001", "entity_id": "376398", "suspicious": "true", "attacker": "true", "network_priority": "5", }, ) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["AlertEntity"] assert result.outputs_key_field == "id" assert result.outputs == outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_case_alert_entity_update_command_network_priority(mock_client, requests_mock): """When network_priority is provided, it should be sent in the request body.""" mock_response = util_load_json("test_data/case_alert_entity_update_success_response.json") patch_mock = requests_mock.patch( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITY'].format(case_id='306082', alert_id='1001', entity_id='376398')}", json=mock_response, ) gcb_case_alert_entity_update_command( mock_client, {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "network_priority": "5"}, ) assert patch_mock.last_request.json().get("networkPriority") == 5 def test_gcb_case_alert_entity_update_command_empty_response(mock_client, requests_mock): """When empty response is received, gcb_case_alert_entity_update_command should return appropriate message.""" requests_mock.patch( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITY'].format(case_id='306082', alert_id='1001', entity_id='376398')}", json={}, ) result = gcb_case_alert_entity_update_command( mock_client, {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "suspicious": "true"} ) assert result.raw_response == {} assert result.outputs is None assert "No entity information found" in result.readable_output @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ( {"case_id": "abc", "alert_id": "1001", "entity_id": "376398", "suspicious": "true"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1001", "entity_id": "376398", "suspicious": "true"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ({"case_id": "306082"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "306082", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "306082", "alert_id": "abc", "entity_id": "376398", "suspicious": "true"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "-5", "entity_id": "376398", "suspicious": "true"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY), ), ({"case_id": "306082", "alert_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("entity_id")), ({"case_id": "306082", "alert_id": "1001", "entity_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("entity_id")), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "abc", "suspicious": "true"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", "abc", ENTITY_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "-5", "suspicious": "true"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", "-5", ENTITY_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "376398"}, MESSAGES["AT_LEAST_ONE_REQUIRED"].format(", ".join(CASE_ALERT_ENTITY_UPDATE_ARGS)), ), ( { "case_id": "306082", "alert_id": "1001", "entity_id": "376398", "suspicious": "true", "network_priority": "-1", }, MESSAGES["INVALID_NON_NEGATIVE_INTEGER"].format(-1, "network_priority"), ), ], ) def test_gcb_case_alert_entity_update_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_entity_update_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_entity_update_command(mock_client, args) assert str(error.value) == expected_error_message def test_gcb_case_alert_entity_update_command_false_boolean_only(mock_client, requests_mock): """When suspicious=false is the only arg, command should succeed (not raise AT_LEAST_ONE_REQUIRED).""" mock_response = util_load_json("test_data/case_alert_entity_update_success_response.json") requests_mock.patch( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITY'].format(case_id='111111', alert_id='2222', entity_id='333333')}", json=mock_response, ) result = gcb_case_alert_entity_update_command( mock_client, {"case_id": "111111", "alert_id": "2222", "entity_id": "333333", "suspicious": "false"}, ) assert result.outputs is not None def test_gcb_case_alert_entity_update_command_network_priority_zero(mock_client, requests_mock): """When network_priority=0, command should succeed (zero is a valid non-negative value).""" mock_response = util_load_json("test_data/case_alert_entity_update_success_response.json") patch_mock = requests_mock.patch( f"{BASE_URL}{ENDPOINTS['CASE_ALERT_INVOLVED_ENTITY'].format(case_id='111111', alert_id='2222', entity_id='333333')}", json=mock_response, ) gcb_case_alert_entity_update_command( mock_client, {"case_id": "111111", "alert_id": "2222", "entity_id": "333333", "network_priority": "0"}, ) assert patch_mock.last_request.json().get("networkPriority") == 0 def test_main_gcb_case_alert_entity_update_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-entity-update command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-entity-update") mocker.patch.object( demisto, "args", return_value={"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "suspicious": "true"}, ) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_entity_update_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_entity_property_add_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_alert_entity_property_add_command should return entity with property added.""" mock_response = util_load_json("test_data/case_alert_entity_property_add_success_response.json") outputs = util_load_json("test_data/case_alert_entity_property_add_context.json") expected_hr = util_load_text_data("test_data/case_alert_entity_property_add_hr.md") endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITY_ADD_PROPERTY"] url = f"{BASE_URL}{endpoint.format(case_id='306082', alert_id='1001', entity_id='376398')}" requests_mock.post(url, json=mock_response) result = gcb_case_alert_entity_property_add_command( mock_client, { "case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "42/72", }, ) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["AlertEntity"] assert result.outputs_key_field == "id" assert result.outputs == outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_case_alert_entity_property_add_command_request_body(mock_client, requests_mock): """When key and value are provided, they should be sent in the request body.""" mock_response = util_load_json("test_data/case_alert_entity_property_add_success_response.json") endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITY_ADD_PROPERTY"] url = f"{BASE_URL}{endpoint.format(case_id='306082', alert_id='1001', entity_id='376398')}" post_mock = requests_mock.post(url, json=mock_response) gcb_case_alert_entity_property_add_command( mock_client, {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "42/72"}, ) assert post_mock.last_request.json() == {"key": "total_score", "value": "42/72"} def test_gcb_case_alert_entity_property_add_command_empty_response(mock_client, requests_mock): """When empty response is received, gcb_case_alert_entity_property_add_command should return appropriate message.""" endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITY_ADD_PROPERTY"] url = f"{BASE_URL}{endpoint.format(case_id='306082', alert_id='1001', entity_id='376398')}" requests_mock.post(url, json={}) result = gcb_case_alert_entity_property_add_command( mock_client, {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "42/72"}, ) assert result.raw_response == {} assert result.outputs is None assert "No entity information found" in result.readable_output @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ( {"case_id": "abc", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "42/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "42/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ({"case_id": "306082"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "306082", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "306082", "alert_id": "abc", "entity_id": "376398", "key": "total_score", "value": "42/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "-5", "entity_id": "376398", "key": "total_score", "value": "42/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY), ), ({"case_id": "306082", "alert_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("entity_id")), ({"case_id": "306082", "alert_id": "1001", "entity_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("entity_id")), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "abc", "key": "total_score", "value": "42/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", "abc", ENTITY_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "-5", "key": "total_score", "value": "42/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", "-5", ENTITY_ID_DISPLAY), ), ({"case_id": "306082", "alert_id": "1001", "entity_id": "376398"}, MESSAGES["REQUIRED_ARGUMENT"].format("key")), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("key"), ), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score"}, MESSAGES["REQUIRED_ARGUMENT"].format("value"), ), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("value"), ), ], ) def test_gcb_case_alert_entity_property_add_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_entity_property_add_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_entity_property_add_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_entity_property_add_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-entity-property-add command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-entity-property-add") mocker.patch.object( demisto, "args", return_value={ "case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "42/72", }, ) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_entity_property_add_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) def test_gcb_case_alert_entity_property_update_command_success(mock_client, requests_mock): """When valid response is received, gcb_case_alert_entity_property_update_command returns entity with property updated.""" mock_response = util_load_json("test_data/case_alert_entity_property_update_success_response.json") outputs = util_load_json("test_data/case_alert_entity_property_update_context.json") expected_hr = util_load_text_data("test_data/case_alert_entity_property_update_hr.md") endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITY_UPDATE_PROPERTY"] url = f"{BASE_URL}{endpoint.format(case_id='306082', alert_id='1001', entity_id='376398')}" requests_mock.post(url, json=mock_response) result = gcb_case_alert_entity_property_update_command( mock_client, { "case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "68/72", }, ) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["AlertEntity"] assert result.outputs_key_field == "id" assert result.outputs == outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_case_alert_entity_property_update_command_request_body(mock_client, requests_mock): """When key and value are provided, they should be sent in the request body.""" mock_response = util_load_json("test_data/case_alert_entity_property_update_success_response.json") endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITY_UPDATE_PROPERTY"] url = f"{BASE_URL}{endpoint.format(case_id='306082', alert_id='1001', entity_id='376398')}" post_mock = requests_mock.post(url, json=mock_response) gcb_case_alert_entity_property_update_command( mock_client, {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "68/72"}, ) assert post_mock.last_request.json() == {"key": "total_score", "value": "68/72"} def test_gcb_case_alert_entity_property_update_command_empty_response(mock_client, requests_mock): """When empty response is received, gcb_case_alert_entity_property_update_command returns appropriate message.""" endpoint = ENDPOINTS["CASE_ALERT_INVOLVED_ENTITY_UPDATE_PROPERTY"] url = f"{BASE_URL}{endpoint.format(case_id='306082', alert_id='1001', entity_id='376398')}" requests_mock.post(url, json={}) result = gcb_case_alert_entity_property_update_command( mock_client, {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "68/72"}, ) assert result.raw_response == {} assert result.outputs is None assert "No entity information found" in result.readable_output @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ( {"case_id": "abc", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "68/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "68/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ({"case_id": "306082"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ({"case_id": "306082", "alert_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_id")), ( {"case_id": "306082", "alert_id": "abc", "entity_id": "376398", "key": "total_score", "value": "68/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "abc", ALERT_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "-5", "entity_id": "376398", "key": "total_score", "value": "68/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("alert_id", "-5", ALERT_ID_DISPLAY), ), ({"case_id": "306082", "alert_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("entity_id")), ({"case_id": "306082", "alert_id": "1001", "entity_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("entity_id")), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "abc", "key": "total_score", "value": "68/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", "abc", ENTITY_ID_DISPLAY), ), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "-5", "key": "total_score", "value": "68/72"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("entity_id", "-5", ENTITY_ID_DISPLAY), ), ({"case_id": "306082", "alert_id": "1001", "entity_id": "376398"}, MESSAGES["REQUIRED_ARGUMENT"].format("key")), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("key"), ), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score"}, MESSAGES["REQUIRED_ARGUMENT"].format("value"), ), ( {"case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("value"), ), ], ) def test_gcb_case_alert_entity_property_update_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_case_alert_entity_property_update_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_case_alert_entity_property_update_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_case_alert_entity_property_update_command_success(mocker, mock_client): """Test main() routes gcb-case-alert-entity-property-update command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-case-alert-entity-property-update") mocker.patch.object( demisto, "args", return_value={ "case_id": "306082", "alert_id": "1001", "entity_id": "376398", "key": "total_score", "value": "68/72", }, ) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_case_alert_entity_property_update_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value) @pytest.mark.parametrize( "args", [ ({"execution_scope": "alert"}), ({"execution_scope": "CASE", "environment": "Production"}), ], ) def test_gcb_playbook_list_command_success(mock_client, requests_mock, args): """When valid response is received, gcb_playbook_list_command should return playbook list.""" mock_response = util_load_json("test_data/playbook_list_success_response.json") expected_outputs = util_load_json("test_data/playbook_list_context.json") expected_hr = util_load_text_data("test_data/playbook_list_hr.md") requests_mock.post(f"{BASE_URL}{ENDPOINTS['LEGACY_PLAYBOOKS']}", json=mock_response) result = gcb_playbook_list_command(mock_client, args) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["Playbook"] assert result.outputs_key_field == "workflowDefinitionIdentifier" assert result.outputs == expected_outputs assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_playbook_list_command_empty_response(mock_client, requests_mock): """When empty payload is received, gcb_playbook_list_command should return no records message.""" requests_mock.post(f"{BASE_URL}{ENDPOINTS['LEGACY_PLAYBOOKS']}", json={}) result = gcb_playbook_list_command(mock_client, {}) assert result.outputs is None assert result.raw_response == {} assert "No enabled playbooks found" in result.readable_output @pytest.mark.parametrize( "args, expected_error_message", [ ( {"execution_scope": "INVALID"}, MESSAGES["VALIDATE_SINGLE_SELECT"].format("execution_scope", ", ".join(VALID_EXECUTION_SCOPES)), ) ], ) def test_gcb_playbook_list_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_playbook_list_command raises ValueError for invalid execution_scope values.""" with pytest.raises(ValueError) as error: gcb_playbook_list_command(mock_client, args) assert str(error.value) == expected_error_message def test_gcb_playbook_attach_command_success(mock_client, requests_mock): """When valid response is received, gcb_playbook_attach_command should return confirmation message.""" args = { "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", } mock_response = {"payload": True} context = util_load_json("test_data/playbook_attach_context.json") expected_hr = ( "Playbook 'Phishing Investigation' successfully attached to alert " "ACCESS DISABLED ACCOUNTS_00000000-0000-0000-0000-000000000002 in case 1001." ) requests_mock.post( f"{BASE_URL}{ENDPOINTS['LEGACY_PLAYBOOK_ATTACH']}", json=mock_response, ) result = gcb_playbook_attach_command(mock_client, args) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["PlaybookAttach"] assert result.outputs_key_field == "alertIdentifier" assert result.outputs == context.get("success") assert result.raw_response == mock_response assert result.readable_output == expected_hr def test_gcb_playbook_attach_command_api_failure(mock_client, requests_mock): """When API returns payload false, gcb_playbook_attach_command should return failure HR.""" args = { "case_id": "1001", "alert_group_identifier": "Access Disabled Accounts_00000000-0000-0000-0000-000000000006", "alert_identifier": "ACCESS DISABLED ACCOUNTS_00000000-0000-0000-0000-000000000007", "playbook_name": "Phishing Investigation", } context = util_load_json("test_data/playbook_attach_context.json") mock_response = {"payload": False} expected_hr = ( "Failed to attach playbook 'Phishing Investigation' to alert " "ACCESS DISABLED ACCOUNTS_00000000-0000-0000-0000-000000000007 in case 1001." ) requests_mock.post(f"{BASE_URL}{ENDPOINTS['LEGACY_PLAYBOOK_ATTACH']}", json=mock_response) result = gcb_playbook_attach_command(mock_client, args) assert result.outputs_prefix == SECOPS_OUTPUT_PATHS["PlaybookAttach"] assert result.outputs_key_field == "alertIdentifier" assert result.raw_response == mock_response assert result.outputs == context.get("failed") assert result.readable_output == expected_hr def test_gcb_playbook_attach_command_api_error(mock_client, requests_mock): """When API returns 400, gcb_playbook_attach_command should raise ValueError.""" args = { "case_id": "1001", "alert_group_identifier": "Access Disabled Accounts_00000000-0000-0000-0000-000000000106", "alert_identifier": "ACCESS DISABLED ACCOUNTS_00000000-0000-0000-0000-000000000207", "playbook_name": "Phishing Investigation", } requests_mock.post( f"{BASE_URL}{ENDPOINTS['LEGACY_PLAYBOOK_ATTACH']}", status_code=404, json={"error": {"message": "Case not found.", "code": 404}}, ) with pytest.raises(ValueError, match="Case not found."): gcb_playbook_attach_command(mock_client, args) @pytest.mark.parametrize( "args, expected_error_message", [ ({}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ({"case_id": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("case_id")), ( {"case_id": "abc", "alert_group_identifier": "grp", "alert_identifier": "alert", "playbook_name": "pb"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "abc", CASE_ID_DISPLAY), ), ( {"case_id": "-5", "alert_group_identifier": "grp", "alert_identifier": "alert", "playbook_name": "pb"}, MESSAGES["INVALID_POSITIVE_INTEGER"].format("case_id", "-5", CASE_ID_DISPLAY), ), ({"case_id": "1001"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_group_identifier")), ({"case_id": "1001", "alert_group_identifier": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_group_identifier")), ({"case_id": "1001", "alert_group_identifier": "grp"}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_identifier")), ( {"case_id": "1001", "alert_group_identifier": "grp", "alert_identifier": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("alert_identifier"), ), ( {"case_id": "1001", "alert_group_identifier": "grp", "alert_identifier": "alert"}, MESSAGES["REQUIRED_ARGUMENT"].format("playbook_name"), ), ( {"case_id": "1001", "alert_group_identifier": "grp", "alert_identifier": "alert", "playbook_name": ""}, MESSAGES["REQUIRED_ARGUMENT"].format("playbook_name"), ), ], ) def test_gcb_playbook_attach_command_invalid_args(mock_client, args, expected_error_message): """Test gcb_playbook_attach_command with invalid arguments.""" with pytest.raises(ValueError) as error: gcb_playbook_attach_command(mock_client, args) assert str(error.value) == expected_error_message def test_main_gcb_playbook_attach_command_success(mocker, mock_client): """Test main() routes gcb-playbook-attach command correctly.""" mocker.patch.object(demisto, "params", return_value=VALID_PARAMS) mocker.patch.object(demisto, "command", return_value="gcb-playbook-attach") mocker.patch.object( demisto, "args", return_value={ "case_id": "1001", "alert_group_identifier": "Access Disabled Accounts_00000000-0000-0000-0000-000000000003", "alert_identifier": "ACCESS DISABLED ACCOUNTS_00000000-0000-0000-0000-000000000004", "playbook_name": "Phishing Investigation", }, ) mocker.patch("GoogleSecOpsCases.validate_configuration_parameters") mocker.patch("GoogleSecOpsCases.Client", return_value=mock_client) mock_command = mocker.patch("GoogleSecOpsCases.gcb_playbook_attach_command", return_value=mock.MagicMock()) mock_return_results = mocker.patch("GoogleSecOpsCases.return_results") main() mock_command.assert_called_once() mock_return_results.assert_called_once_with(mock_command.return_value)