GoogleThreatIntelligenceASMIssues
This integration allows the creation of incidents based on ASM Issues from Google Threat Intelligence.
Data Enrichment & Threat Intelligence · GoogleThreatIntelligence
Details
| ID | GoogleThreatIntelligenceASMIssues |
|---|---|
| Provider | |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Google Threat Intelligence - ASM Issues
This integration allows the creation of incidents based on ASM Issues from Google Threat Intelligence.
Configure Google Threat Intelligence - ASM Issues in Cortex
| Parameter | Description | Required |
|---|---|---|
| API Key | True | |
| Fetch incidents | False | |
| Max Fetch | Maximum number of Issues to fetch each time. Maximum value is 200. | False |
| First Fetch Time | The date or relative timestamp from which to begin fetching Issues. 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 2025, 01 May 2025 04:45:33, 2025-05-17T14:05:44Z. |
False |
| Mirroring Direction | The mirroring direction in which to mirror the details. You can mirror “Outgoing” (from XSOAR to GTI) direction for ASM Issues. Cortex XSOAR only parameter. | False |
| Mirror tag for notes | The tag value should be used to mirror the issue note by adding the same tag in the notes. | False |
| Project ID | Provide the project ID to fetch issues for a specific project. Note: The specified project ID will be used as a default value for ASM Issue commands. |
False |
| Search String | Search String to filter out the ASM Issues. For Example: collection:google severity:5 status_new:open scoped:true entity_type:domain Note: The fields last_seen_after, last_seen_before, and first_seen_after will be ignored from the provided search string. |
|
| Use system proxy settings | False | |
| Trust any certificate (not secure) | False | |
| Incident type |
Acquiring your API key
Your API key can be found in your GoogleThreatIntelligence account user menu, clicking on your avatar:

Your API key carries all your privileges, so keep it secure and don’t share it with anyone.
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.
gti-asm-issue-list
Search the ASM Issues with provided filter arguments.
Base Command
gti-asm-issue-list
Input
| Argument Name | Description | Required |
|---|---|---|
| project_id | Specify the project ID for the project. Note:If no value is provided for the project ID, it will be taken from the configuration parameters. |
Optional |
| search_string | Specify search string for listing ASM Issues. Note: If no value is provided, all issues for the project will be returned. |
Optional |
| page_size | Specify the desired page size for the request. Maximum value is 1000. Default is 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleThreatIntelligenceASMIssues.Issues.id | String | Unique identifier for the issue in Google Threat Intelligence. |
| GoogleThreatIntelligenceASMIssues.Issues.uid | String | Internal UID assigned to the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.uuid | String | Universally unique identifier (UUID) for the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.description | String | Detailed description of the identified issue. |
| GoogleThreatIntelligenceASMIssues.Issues.dynamic_id | Number | Dynamic numerical identifier for tracking the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.name | String | Human-readable name of the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.pretty_name | String | Enhanced, formatted name for display purposes. |
| GoogleThreatIntelligenceASMIssues.Issues.upstream | String | Source or upstream system where the issue originated. |
| GoogleThreatIntelligenceASMIssues.Issues.last_seen | Date | Timestamp when the issue was last observed. |
| GoogleThreatIntelligenceASMIssues.Issues.first_seen | Date | Timestamp when the issue was first detected. |
| GoogleThreatIntelligenceASMIssues.Issues.entity_uid | String | Unique identifier of the affected entity. |
| GoogleThreatIntelligenceASMIssues.Issues.entity_type | String | Type of entity affected (e.g., domain, IP, host). |
| GoogleThreatIntelligenceASMIssues.Issues.entity_name | String | Name of the affected entity. |
| GoogleThreatIntelligenceASMIssues.Issues.alias_group | String | Group of related entities or aliases associated with the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.collection | String | Collection or dataset name where the issue belongs. |
| GoogleThreatIntelligenceASMIssues.Issues.collection_uuid | String | Unique UUID of the associated collection. |
| GoogleThreatIntelligenceASMIssues.Issues.collection_type | String | Type of collection where the issue is categorized. |
| GoogleThreatIntelligenceASMIssues.Issues.organization_uuid | String | UUID of the organization linked to the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.pretty_name | String | User-friendly name summarizing the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.severity | Number | Severity level assigned to the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.scoped | Boolean | Indicates if the issue is scoped to a specific context or asset. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.confidence | String | Confidence score indicating the reliability of the issue detection. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.status | String | Current status of the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.category | String | Category of the issue based on threat type. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.identifiers.name | String | Name of an identifier associated with the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.identifiers.type | String | Type of identifier linked to the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.status_new | String | Updated status of the issue based on latest assessment. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.status_new_detailed | String | Detailed description of the updated issue status. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.ticket_list | List | List of linked tickets associated with the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.tags | List | Tags associated with the issue for classification. |
| GoogleThreatIntelligenceASMIssues.Issues.cisa_known_exploited | Boolean | Indicates whether the issue is part of CISA’s Known Exploited Vulnerabilities list. |
| GoogleThreatIntelligenceASMIssues.Issues.epss_v2_score_lte | Number | EPSS v2 score indicating likelihood of exploitation (less than or equal). |
| GoogleThreatIntelligenceASMIssues.Issues.epss_v2_percentile_gte | Number | EPSS v2 percentile indicating exploitation probability (greater than or equal). |
Command example
!gti-asm-issue-list search_string="collection:google" page_size=2
Context Example
{
"GoogleThreatIntelligenceASMIssues": {
"Issues": [
{
"id": "dummy_uid_01",
"uid": "dummy_uid_01",
"uuid": "dummy_uuid_01",
"description": "A cookie was found, missing the 'HttpOnly' attribute. HttpOnly is a flag included in a Set-Cookie HTTP response header.",
"dynamic_id": 10101011,
"name": "insecure_cookie_httponly_attribute",
"pretty_name": "Insecure Cookie (Missing 'HttpOnly' Attribute)",
"upstream": "intrigue",
"last_seen": "2025-07-14T15:18:51.000Z",
"first_seen": "2025-06-29T00:46:43.000Z",
"entity_uid": "dummy_entity_uid_01",
"entity_type": "Intrigue::Entity::Uri",
"entity_name": "https://www.example_entity.com",
"alias_group": "dummy_alias_group_01",
"collection": "google",
"collection_uuid": "dummy_collection_uuid_01",
"collection_type": "pre_collection",
"organization_uuid": "test",
"summary": {
"pretty_name": "Insecure Cookie (Missing 'HttpOnly' Attribute)",
"severity": 5,
"scoped": true,
"confidence": "confirmed",
"status": "closed_benign",
"category": "application",
"identifiers": "123",
"status_new": "closed",
"status_new_detailed": "benign",
"ticket_list": [
"ticket"
]
},
"tags": [
"test1"
]
},
{
"id": "dummy_uid_02",
"uid": "dummy_uid_02",
"uuid": "dummy_uuid_02",
"description": "A cookie was found, missing the 'HttpOnly' attribute.",
"dynamic_id": 10101010,
"name": "insecure_cookie_httponly_attribute",
"pretty_name": "Insecure Cookie (Missing 'HttpOnly' Attribute)",
"upstream": "intrigue",
"last_seen": "2025-07-14T15:18:51.000Z",
"first_seen": "2025-06-29T00:46:43.000Z",
"entity_uid": "dummy_entity_uid_02",
"entity_type": "Intrigue::Entity::Uri",
"entity_name": "http://www.exmaple_2.com",
"alias_group": "dummy_alias_group_02",
"collection": "google",
"collection_uuid": "dummy_collection_uuid_02",
"collection_type": "pre_collection",
"organization_uuid": "test",
"summary": {
"pretty_name": "Insecure Cookie (Missing 'HttpOnly' Attribute)",
"severity": 5,
"scoped": true,
"confidence": "confirmed",
"status": "open_new",
"category": "application",
"identifiers": "123",
"status_new": "open",
"status_new_detailed": "new",
"ticket_list": [
"ticket"
]
},
"tags": [
"Test2"
]
}
]
}
}
Human Readable Output
ASM Issues
Issue ID Issue Name Issue Description Status Severity Entity Name Entity uid Entity Type Collection Confidence Last Seen First Seen Tags
dummy_uid_01 Insecure Cookie (Missing ‘HttpOnly’ Attribute) A cookie was found, missing the ‘HttpOnly’ attribute. HttpOnly is a flag included in a Set-Cookie HTTP response header. Benign 5 https://www.example_entity.com dummy_entity_uid_01 Intrigue::Entity::Uri confirmed 2025-07-14T15:18:51.000Z 2025-06-29T00:46:43.000Z test1 dummy_uid_02 Insecure Cookie (Missing ‘HttpOnly’ Attribute) A cookie was found, missing the ‘HttpOnly’ attribute. Open 5 http://www.exmaple_2.com dummy_entity_uid_02 Intrigue::Entity::Uri confirmed 2025-07-14T15:18:51.000Z 2025-06-29T00:46:43.000Z Test2
gti-asm-issue-get
Get a particular ASM Issue by ID.
Base Command
gti-asm-issue-get
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Specify the ID of the issue. Note: Use gti-asm-issue-list to retrive the Issue ID. |
Required |
| project_id | Specify the project ID for the project. Note:If no value is provided for the project ID, it will be taken from the configuration parameters. |
Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleThreatIntelligenceASMIssues.Issues.uuid | String | Universally unique identifier (UUID) of the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.dynamic_id | Number | Dynamic numerical identifier assigned to the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.entity_uid | String | Unique identifier of the affected entity. |
| GoogleThreatIntelligenceASMIssues.Issues.alias_group | String | Group of related aliases associated with the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.category | String | Threat category associated with the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.confidence | String | Confidence score indicating the reliability of detection. |
| GoogleThreatIntelligenceASMIssues.Issues.description | String | Detailed description of the identified issue. |
| GoogleThreatIntelligenceASMIssues.Issues.details.name | String | Name associated with the issue details. |
| GoogleThreatIntelligenceASMIssues.Issues.details.source | String | Data source from which the issue details are derived. |
| GoogleThreatIntelligenceASMIssues.Issues.details.status | String | Current status of the issue details. |
| GoogleThreatIntelligenceASMIssues.Issues.details.category | String | Category of the issue details. |
| GoogleThreatIntelligenceASMIssues.Issues.details.severity | String | Severity level of the issue details. |
| GoogleThreatIntelligenceASMIssues.Issues.details.mx_records.host | String | Host name from the MX record. |
| GoogleThreatIntelligenceASMIssues.Issues.details.mx_records.priority | String | Priority value from the MX record. |
| GoogleThreatIntelligenceASMIssues.Issues.details.references.uri | String | Reference URI linked to the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.details.references.type | String | Type of reference for the provided URI. |
| GoogleThreatIntelligenceASMIssues.Issues.details.description | String | Description provided in the issue details. |
| GoogleThreatIntelligenceASMIssues.Issues.details.pretty_name | String | Readable and formatted name for the issue details. |
| GoogleThreatIntelligenceASMIssues.Issues.first_seen | Date | Timestamp when the issue was first detected. |
| GoogleThreatIntelligenceASMIssues.Issues.identifiers | List | List of identifiers associated with the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.last_seen | Date | Timestamp when the issue was last observed. |
| GoogleThreatIntelligenceASMIssues.Issues.name | String | Name assigned to the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.pretty_name | String | Human-readable formatted name of the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.scoped | Boolean | Indicates whether the issue is scoped to a specific asset or context. |
| GoogleThreatIntelligenceASMIssues.Issues.severity | Number | Severity score assigned to the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.source | String | Source from which the issue originates. |
| GoogleThreatIntelligenceASMIssues.Issues.status | String | Current status of the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.ticket_list | List | List of tickets linked to this issue. |
| GoogleThreatIntelligenceASMIssues.Issues.type | String | Type of issue detected. |
| GoogleThreatIntelligenceASMIssues.Issues.uid | String | Internal unique ID of the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.upstream | String | Upstream system or source responsible for reporting the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.created_at | Date | Timestamp when the issue was created. |
| GoogleThreatIntelligenceASMIssues.Issues.updated_at | Date | Timestamp when the issue was last updated. |
| GoogleThreatIntelligenceASMIssues.Issues.collection_id | Number | Numeric ID of the collection associated with the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.elasticsearch_mappings_hash | String | Hash value representing Elasticsearch mappings for the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.cisa_known_exploited | Boolean | Indicates if the issue is part of CISA’s Known Exploited Vulnerabilities list. |
| GoogleThreatIntelligenceASMIssues.Issues.epss_v2_score_lte | Number | EPSS v2 score showing likelihood of exploitation (less than or equal). |
| GoogleThreatIntelligenceASMIssues.Issues.epss_v2_percentile_gte | Number | EPSS v2 percentile representing exploitation probability (greater than or equal). |
| GoogleThreatIntelligenceASMIssues.Issues.entity_id | Number | Numeric ID of the associated entity. |
| GoogleThreatIntelligenceASMIssues.Issues.collection | String | Name of the collection where the issue belongs. |
| GoogleThreatIntelligenceASMIssues.Issues.collection_type | String | Type of collection to which the issue is linked. |
| GoogleThreatIntelligenceASMIssues.Issues.collection_uuid | String | UUID of the associated collection. |
| GoogleThreatIntelligenceASMIssues.Issues.organization_uuid | String | UUID of the organization associated with the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.entity_name | String | Name of the affected entity. |
| GoogleThreatIntelligenceASMIssues.Issues.entity_type | String | Type of the affected entity (e.g., domain, IP, asset). |
| GoogleThreatIntelligenceASMIssues.Issues.summary.pretty_name | String | User-friendly summary name of the issue. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.severity | Number | Severity level defined in the issue summary. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.scoped | Boolean | Indicates whether the summary is scoped to a specific asset or context. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.confidence | String | Confidence score provided in the issue summary. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.status | String | Status value specified in the issue summary. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.category | String | Category defined in the issue summary. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.identifiers | List | Identifiers included in the issue summary. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.status_new | String | Updated status of the issue summary. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.status_new_detailed | String | Detailed description of the updated issue summary status. |
| GoogleThreatIntelligenceASMIssues.Issues.summary.ticket_list | List | List of tickets associated with the issue summary. |
| GoogleThreatIntelligenceASMIssues.Issues.tags | List | Tags assigned to the issue for categorization. |
Command example
!gti-asm-issue-get issue_id=dummy_uid
Context Example
{
"GoogleThreatIntelligenceASMIssues": {
"Issues": {
"uuid": "dummy_uuid",
"dynamic_id": 210054431,
"entity_uid": "dummy_entity_uid",
"alias_group": "4898048",
"category": "misconfiguration",
"confidence": "confirmed",
"description": "Any CA is able to generate a certificate for this domain, increasing the risk of exposure if that CA is compromised.",
"details": {
"name": "dns_caa_policy_missing",
"task": "tasks",
"added": "dummy_added",
"proof": "dummy_proof",
"status": "dummy_status",
"category": "dummy_category",
"severity": 5,
"references": [
{
"uri": "https://www.example.com/DNS_Certification_Authority_Authorization",
"type": "description"
},
{
"uri": "https://www.example.com/caa/",
"type": "remediation"
}
],
"description": "Any CA is able to generate a certificate for this domain, increasing the risk of exposure if that CA is compromised.",
"pretty_name": "Domain is missing a CAA record",
"remediation": "Add a CAA record, setting the policy for this domain."
},
"first_seen": "2024-12-18T15:43:48.000Z",
"identifiers": "test",
"last_seen": "2025-02-11T16:55:42.000Z",
"name": "dns_caa_policy_missing",
"pretty_name": "Domain is missing a CAA record",
"scoped": true,
"severity": 5,
"source": "intrigue",
"status": "open_new",
"ticket_list": ["ticket"],
"type": "standard",
"uid": "dummy_uid",
"upstream": "intrigue",
"created_at": "2025-02-11T16:56:52.734Z",
"updated_at": "2024-12-19T00:00:00.000Z",
"collection_id": 181421,
"elasticsearch_mappings_hash": "dummy_elasticsearch_mappings_hash",
"cisa_known_exploited": false,
"epss_v2_score_lte": 4,
"epss_v2_percentile_gte": 4,
"entity_id": -522945081,
"collection": "testdata",
"collection_type": "pre_collection",
"collection_uuid": "dummy_collection_uuid",
"organization_uuid": "dummy_organization_uuid",
"entity_name": "testdata.ai",
"entity_type": "Intrigue::Entity::Domain",
"summary": {
"pretty_name": "Domain is missing a CAA record",
"severity": 5,
"scoped": true,
"confidence": "confirmed",
"status": "open_new",
"category": "misconfiguration",
"identifiers": "test",
"status_new": "open",
"status_new_detailed": "new",
"ticket_list": ["ticket"]
},
"tags": ["Test"]
}
}
}
Human Readable Output
ASM Issue
Issue ID Issue Name Issue Description Status Severity Entity Name Entity uid Entity Type Collection Confidence Last Seen First Seen Tags dummy_uid Domain is missing a CAA record Any CA is able to generate a certificate for this domain, increasing the risk of exposure if that CA is compromised. Open 5 testdata.ai dummy_entity_uid Intrigue::Entity::Domain testdata confirmed 2025-02-11T16:55:42.000Z 2024-12-18T15:43:48.000Z Test
gti-asm-issue-status-update
Update the status of an ASM Issue.
Base Command
gti-asm-issue-status-update
Input
| Argument Name | Description | Required |
|---|---|---|
| issue_id | Specify the ID of the issue. Note: Use gti-asm-issue-list to retrive the Issue ID. |
Required |
| project_id | Specify the project ID for the project. Note:If no value is provided for the project ID, it will be taken from the configuration parameters. |
Optional |
| status | Specify status of the issues to be updated. Possible values are: Open, Triaged, In Progress, Closed, Mitigated, Resolved, Duplicate, Out of Scope, False Positive, Risk Accepted, Benign, Unable to Reproduce, Track Externally. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GoogleThreatIntelligenceASMIssues.Issues.uid | String | Unique identifier for the issue in Google Threat Intelligence. |
| GoogleThreatIntelligenceASMIssues.Issues.success | Boolean | Indicates whether the API request to fetch issue details was successful. |
| GoogleThreatIntelligenceASMIssues.Issues.message | String | Response message returned by the API, providing additional information. |
| GoogleThreatIntelligenceASMIssues.Issues.result | String | Result status or outcome returned by the API for the issue request. |
Command example
!gti-asm-issue-status-update issue_id="dummy_uid" status="resolved"
Context Example
{
"GoogleThreatIntelligenceASMIssues": {
"Issues": {
"message": "Successfully reported status as closed_resolved",
"result": "closed_resolved",
"success": true,
"uid": "dummy_uid"
}
}
}
Human Readable Output
ASM Issue Status Updated Successfully
Issue ID Status dummy_uid Resolved
Configuration parameters
credentials— (required)isFetch— Fetch incidentsmax_fetch— Max Fetchfirst_fetch— First Fetch Timemirror_direction— Mirroring Directionnote_tag— Mirror tag for notesproject_id— Project IDsearch_string— Search Stringproxy— Use system proxy settingsinsecure— Trust any certificate (not secure)incidentType— Incident typeincidentFetchInterval— Incidents Fetch Interval
Commands (3)
-
gti-asm-issue-getGet a particular ASM Issue by ID.
-
gti-asm-issue-listSearch the ASM Issues with provided filter arguments.
-
gti-asm-issue-status-updateUpdate the status of an ASM Issue.
from GoogleThreatIntelligenceASMIssues import ( Client, BASE_URL, ENDPOINTS, ERROR_MESSAGES, MESSAGES, ASM_ISSUE_STATUS_HUMAN_READABLE, ) from CommonServerPython import * # noqa # pylint: disable=unused-wildcard-import import json import pytest # Helper Functions def util_load_json(path): """Load JSON data from file.""" with open(path, encoding="utf-8") as f: return json.loads(f.read()) @pytest.fixture def mock_client(mocker): """Create a mocked client for testing.""" return Client(verify_certificate=False, proxy=False, api_key="test_api_key", project_id="1234") def test_test_module_success(mock_client, requests_mock): """Test test_module function returns 'ok' when API call succeeds.""" from GoogleThreatIntelligenceASMIssues import test_module requests_mock.get( f'{BASE_URL}/{ENDPOINTS["issue_list"].format("status_new:open")}', json={"success": True, "result": {"hits": []}}, status_code=200, ) result = test_module(client=mock_client) # Verify the function returns "ok" assert result == "ok" def test_test_module_invalid_api_key(mock_client, requests_mock): """Test test_module function returns 'ok' when API call succeeds.""" from GoogleThreatIntelligenceASMIssues import test_module requests_mock.get( f'{BASE_URL}/{ENDPOINTS["issue_list"].format("status_new:open")}', json={"error": {"code": "WrongCredentialsError", "message": "Wrong API key"}}, status_code=401, ) with pytest.raises(DemistoException) as e: test_module(client=mock_client) assert ( str(e.value) == "401 Unauthorized request: Invalid API key provided " "{'error': {'code': 'WrongCredentialsError', 'message': 'Wrong API key'}}." ) def test_main_test_module_success(mocker, requests_mock): """ Given: - Valid parameters and test-module command. When: - Running the main function. Then: - Validate that test_module is called and returns 'ok'. """ from GoogleThreatIntelligenceASMIssues import main # Mock demisto functions mock_params = {"credentials": {"password": "test_api_key"}, "insecure": False, "proxy": False} mock_demisto = mocker.patch("GoogleThreatIntelligenceASMIssues.demisto") mock_demisto.params.return_value = mock_params mock_demisto.command.return_value = "test-module" mock_demisto.args.return_value = {} mock_demisto.debug = mocker.Mock() # Mock return_results and return_error mock_return_results = mocker.patch("GoogleThreatIntelligenceASMIssues.return_results") mock_return_error = mocker.patch("GoogleThreatIntelligenceASMIssues.return_error") # Mock the API call for test_module requests_mock.get( f'{BASE_URL}/{ENDPOINTS["issue_list"].format("status_new:open")}', json={"success": True, "result": {"hits": []}}, status_code=200, ) # Call main main() # Assertions mock_demisto.command.assert_called_once() mock_return_results.assert_called_once_with("ok") mock_return_error.assert_not_called() @pytest.mark.parametrize( "command, args, mock_api_response, expected_result_type", [ ("test-module", {}, {}, str), ("gti-asm-issue-get", {"issue_id": "test_123"}, "asm_issue_get.json", type(None)), ], ) def test_main_try_block_success_paths(mocker, requests_mock, command, args, mock_api_response, expected_result_type): """ Given: - Valid parameters and different commands. When: - Running the main function try block. Then: - Validate that the appropriate command is executed and return_results is called. """ from GoogleThreatIntelligenceASMIssues import main # Mock demisto functions mock_params = {"credentials": {"password": "test_api_key"}, "insecure": False, "proxy": False, "project_id": "test_project"} mock_demisto = mocker.patch("GoogleThreatIntelligenceASMIssues.demisto") mock_demisto.params.return_value = mock_params mock_demisto.command.return_value = command mock_demisto.args.return_value = args mock_demisto.debug = mocker.Mock() # Mock return_results and return_error mock_return_results = mocker.patch("GoogleThreatIntelligenceASMIssues.return_results") mock_return_error = mocker.patch("GoogleThreatIntelligenceASMIssues.return_error") # Mock API responses based on command if command == "test-module": requests_mock.get( f'{BASE_URL}/{ENDPOINTS["issue_list"].format("status_new:open")}', json={"success": True, "result": {"hits": []}}, status_code=200, ) elif command == "gti-asm-issue-get": mock_response = util_load_json(f"test_data/{mock_api_response}") requests_mock.get(f"{BASE_URL}/{ENDPOINTS['issue_get'].format(args['issue_id'])}", json=mock_response) # Call main main() # Assertions mock_return_results.assert_called_once() mock_return_error.assert_not_called() # Verify the result type if needed if command == "test-module": mock_return_results.assert_called_with("ok") def test_main_try_block_unknown_command_exception(mocker): """ Given: - An unknown command that raises NotImplementedError. When: - Running the main function with unknown command. Then: - Validate that NotImplementedError is caught and return_error is called. """ from GoogleThreatIntelligenceASMIssues import main # Mock demisto functions mock_params = {"credentials": {"password": "test_api_key"}, "insecure": False, "proxy": False, "project_id": "test_project"} mock_demisto = mocker.patch("GoogleThreatIntelligenceASMIssues.demisto") mock_demisto.params.return_value = mock_params mock_demisto.command.return_value = "unknown-command" mock_demisto.args.return_value = {} # Mock return functions mock_return_results = mocker.patch("GoogleThreatIntelligenceASMIssues.return_results") mock_return_error = mocker.patch("GoogleThreatIntelligenceASMIssues.return_error") # Call main main() # Verify exception handling mock_return_error.assert_called_once() error_message = mock_return_error.call_args[0][0] # Verify error message contains expected content assert "Failed to execute unknown-command command" in error_message assert "Command unknown-command is not implemented" in error_message mock_return_results.assert_not_called() def test_get_asm_issue_command_success(mock_client, requests_mock): """ Given: - Valid parameters and get-asm-issue command. When: - Running the main function with get-asm-issue command. Then: - Validate that get_asm_issue_command is called and return_results is called. """ from GoogleThreatIntelligenceASMIssues import gti_asm_issue_get_command mock_response = util_load_json("test_data/asm_issue_get.json") with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/asm_issue_get_human_readable.md")) as f: asm_issue_get_hr = f.read() requests_mock.get(f"{BASE_URL}/{ENDPOINTS['issue_get'].format('dummy_uid')}", json=mock_response) results = gti_asm_issue_get_command(client=mock_client, args={"issue_id": "dummy_uid", "project_id": "test_project"}) assert results.readable_output == asm_issue_get_hr assert results.outputs == [mock_response["result"]] @pytest.mark.parametrize( "args, exception, error", [({"issue_id": ""}, ValueError, MESSAGES["REQUIRED_ARGUMENT"].format("issue_id"))] ) def test_gti_asm_issue_get_command_when_invalid_input(args, mock_client, exception, error): """ Given: - An invalid input When: - Running the !gti-asm-issue-get command Then: - Validate the command results are valid """ from GoogleThreatIntelligenceASMIssues import gti_asm_issue_get_command with pytest.raises(exception) as e: gti_asm_issue_get_command(mock_client, args) assert str(e.value) == error def test_gti_asm_issue_get_command_no_record_found(mock_client, requests_mock): """ Given: - Valid parameters and get-asm-issue command. When: - Running the main function with get-asm-issue command. Then: - Validate that get_asm_issue_command is called and return_results is called. """ from GoogleThreatIntelligenceASMIssues import gti_asm_issue_get_command requests_mock.get(f"{BASE_URL}/{ENDPOINTS['issue_get'].format('dummy_uid')}", json={}) results = gti_asm_issue_get_command(client=mock_client, args={"issue_id": "dummy_uid", "project_id": "test_project"}) assert results.readable_output == "No ASM Issue was found for the given argument(s)." assert results.outputs is None def test_gti_asm_issue_status_update_command_success(mock_client, requests_mock): """ Given: - Valid parameters and get-asm-issue-status-update command. When: - Running the main function with get-asm-issue-status-update command. Then: - Validate that get_asm_issue_command is called and return_results is called. """ from GoogleThreatIntelligenceASMIssues import gti_asm_issue_status_update_command mock_response = util_load_json("test_data/asm_issue_update_status.json") with open( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/asm_issue_status_update_human_readable.md") ) as f: asm_issue_status_update_hr = f.read() requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_status_update'].format('dummy_uid')}", json=mock_response) results = gti_asm_issue_status_update_command( client=mock_client, args={"issue_id": "dummy_uid", "project_id": "test_project", "status": "Open"} ) assert results.readable_output == asm_issue_status_update_hr assert results.outputs == [mock_response] def test_gti_asm_issue_status_update_command_no_record_found(mock_client, requests_mock): """ Given: - Valid parameters and get-asm-issue-status-update command. When: - Running the main function with get-asm-issue-status-update command. Then: - Validate that get_asm_issue_command is called and return_results is called. """ from GoogleThreatIntelligenceASMIssues import gti_asm_issue_status_update_command requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_status_update'].format('dummy_uid')}", json={"success": False}) results = gti_asm_issue_status_update_command( client=mock_client, args={"issue_id": "dummy_uid", "project_id": "test_project", "status": "Open"} ) assert results.readable_output == "No ASM Issue status was updated." assert results.outputs is None @pytest.mark.parametrize( "args, exception, error", [ ({"issue_id": "", "status": "Open"}, ValueError, MESSAGES["REQUIRED_ARGUMENT"].format("issue_id")), ({"issue_id": "dummy_uid", "status": ""}, ValueError, MESSAGES["REQUIRED_ARGUMENT"].format("status")), ( {"issue_id": "dummy_uid", "status": "open_new"}, ValueError, ERROR_MESSAGES["INVALID_ARGUMENT"].format("open_new", "status", ASM_ISSUE_STATUS_HUMAN_READABLE), ), ], ) def test_gti_asm_issue_status_update_command_when_invalid_input(args, mock_client, exception, error): """ Given: - Invalid parameters and get-asm-issue-status-update command. When: - Running the main function with get-asm-issue-status-update command. Then: - Validate that get_asm_issue_command is called and return_results is called. """ from GoogleThreatIntelligenceASMIssues import gti_asm_issue_status_update_command with pytest.raises(exception) as e: gti_asm_issue_status_update_command(mock_client, args) assert str(e.value) == error def test_gti_asm_issue_list_command_success(mock_client, requests_mock): """ Given: - Valid parameters and get-asm-issue-list command. When: - Running the main function with get-asm-issue-list command. Then: - Validate that get_asm_issue_command is called and return_results is called. """ from GoogleThreatIntelligenceASMIssues import gti_asm_issue_list_command mock_response = util_load_json("test_data/asm_issue_list.json") with open(os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/asm_issue_list_human_readable.md")) as f: asm_issue_list_hr = f.read() search_string = "collection:google entity_type:uri" requests_mock.get(f"{BASE_URL}/{ENDPOINTS['issue_list'].format(search_string)}", json=mock_response) results = gti_asm_issue_list_command(client=mock_client, args={"project_id": "test_project", "search_string": search_string}) assert results.readable_output == asm_issue_list_hr assert results.outputs == mock_response["result"]["hits"] def test_gti_asm_issue_list_command_no_record_found(mock_client, requests_mock): """ Given: - Valid parameters and get-asm-issue-list command. When: - Running the main function with get-asm-issue-list command. Then: - Validate that get_asm_issue_command is called and return_results is called. """ from GoogleThreatIntelligenceASMIssues import gti_asm_issue_list_command requests_mock.get( f"{BASE_URL}/{ENDPOINTS['issue_list'].format('collection:actiontocreatecollection_i4qlbqg')}", json={"success": True, "result": {"hits": []}}, ) results = gti_asm_issue_list_command( client=mock_client, args={"project_id": "test_project", "search_string": "collection:actiontocreatecollection_i4qlbqg"} ) assert results.readable_output == "No ASM Issues were found for the given argument(s)." assert results.outputs is None @pytest.mark.parametrize( "args, exception, error", [ ( {"search_string": "collection:abc"}, ValueError, "Failed to retrieve ASM issues from Google Threat Intelligence API." " Verify the search string and project ID are correct, and try again." " Provided Search string: 'collection:abc', Project ID: '1234'", ), ({"page_size": "10000"}, ValueError, ERROR_MESSAGES["INVALID_PAGE_SIZE"].format(10000)), ({"page_size": "-1"}, ValueError, ERROR_MESSAGES["INVALID_PAGE_SIZE"].format(-1)), ], ) def test_gti_asm_issue_list_command_when_invalid_input(mock_client, requests_mock, args, exception, error): """ Given: - Invalid parameters and get-asm-issue-list command. When: - Running the main function with get-asm-issue-list command. Then: - Validate that get_asm_issue_command is called and return_results is called. """ from GoogleThreatIntelligenceASMIssues import gti_asm_issue_list_command requests_mock.get(f"{BASE_URL}/{ENDPOINTS['issue_list'].format('collection:abc')}", json={"success": False}) with pytest.raises(exception) as e: gti_asm_issue_list_command(mock_client, args) assert str(e.value) == error def test_fetch_incidents_asm_issue_test_connectivity(mock_client, requests_mock, mocker): """ Given: - Valid parameters and fetch-incidents command. When: - Running the main function with fetch-incidents command. Then: - Validate that fetch_incidents is called and return_results is called. """ from GoogleThreatIntelligenceASMIssues import test_module requests_mock.get( f"{BASE_URL}/{ENDPOINTS['issue_list'].format('last_seen_after:2025-09-29T08:59:20.494Z')}", json={"success": True, "result": {"hits": []}}, ) params = { "isFetch": True, "max_fetch": "10", "first_fetch": "2025-09-29T08:59:20.494Z", "mirror_direction": "Outgoing", "note_tag": "note", } # Mock demisto.params() to return our test parameters mocker.patch.object(demisto, "params", return_value=params) result = test_module(mock_client) assert result == "ok" @pytest.mark.parametrize( "max_fetch_value, expected_error", [ (0, ERROR_MESSAGES["INVALID_MAX_FETCH"].format(0)), (-1, ERROR_MESSAGES["INVALID_MAX_FETCH"].format(-1)), (250, ERROR_MESSAGES["INVALID_MAX_FETCH"].format(250)), ], ) def test_fetch_incidents_asm_issue_max_fetch_params_invalid(mock_client, max_fetch_value, expected_error): """ Test fetch_incidents with invalid max_fetch values. Given: - Invalid max_fetch parameters (negative, zero, exceeding limit) When: - Running fetch_incidents function Then: - Validate that ValueError is raised with appropriate error messages """ from GoogleThreatIntelligenceASMIssues import fetch_incidents params = { "first_fetch": "3 days", "max_fetch": max_fetch_value, "search_string": "", "mirror_direction": "Outgoing", "note_tag": "note", } with pytest.raises(ValueError) as e: fetch_incidents(client=mock_client, last_run={}, params=params) assert str(e.value) == expected_error def test_fetch_incidents_asm_issue_success_with_no_last_run(mock_client, requests_mock, mocker): """ Test fetch_incidents with no last_run. Given: - No last_run provided When: - Running fetch_incidents function Then: - Validate that fetch_incidents returns expected results """ from GoogleThreatIntelligenceASMIssues import fetch_incidents search_string = "collection:google severity:5 last_seen_after:2025-09-30T09:45:30.711Z" params = { "first_fetch": "2025-09-01T09:45:30.711Z", "max_fetch": "4", "search_string": search_string, "project_id": "12345", "mirror_direction": "Outgoing", "note_tag": "note", } mock_response = util_load_json("test_data/fetch_asm_issue_success_response.json") fetched_incidents = util_load_json( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_asm_issue_success_incidents.json") ) issue_ids = [alert["uid"] for alert in mock_response["result"]["hits"]] # Mock demisto.params() to return our test parameters mocker.patch.object(demisto, "params", return_value=params) requests_mock.get( f"{BASE_URL}/{ENDPOINTS['issue_list'].format('collection:google severity:5 last_seen_after:2025-09-01T09:45:30.711Z')}", json=mock_response, ) # Call fetch_incidents incidents, new_last_run = fetch_incidents(client=mock_client, last_run={}, params=params) # Assert expected results assert incidents == fetched_incidents assert new_last_run["max_fetch"] == 4 assert new_last_run["asm_latest_issue_last_seen"] == "2025-07-14T15:18:51.000Z" assert new_last_run["index"] == 1 assert new_last_run["issue_ids"] == issue_ids assert new_last_run["search_string"] == params["search_string"] def test_fetch_incidents_asm_issue_success_with_no_issue_data(mock_client, requests_mock, mocker): """ Test fetch_incidents with no data. Given: - No data in the response When: - Running fetch_incidents function Then: - Validate that fetch_incidents returns expected results """ from GoogleThreatIntelligenceASMIssues import fetch_incidents params = { "first_fetch": "2025-06-30T09:45:30.711Z", "max_fetch": "4", "project_id": "12345", "mirror_direction": "Outgoing", "note_tag": "note", } mock_response = {"success": True, "result": {"hits": []}} last_run = { "max_fetch": 4, "asm_latest_issue_last_seen": "2025-07-14T15:18:51.000Z", "index": 1, "issue_ids": ["dummy_uid"], "search_string": "", } # Mock demisto.params() to return our test parameters mocker.patch.object(demisto, "params", return_value=params) requests_mock.get( f"{BASE_URL}/{ENDPOINTS['issue_list'].format('last_seen_after:2025-07-14T15:18:51.000Z')}", json=mock_response ) # Call fetch_incidents incidents, new_last_run = fetch_incidents(client=mock_client, last_run=last_run, params=params) # Assert expected results assert incidents == [] assert new_last_run["max_fetch"] == 4 assert new_last_run["asm_latest_issue_last_seen"] == "2025-07-14T15:18:51.000Z" assert new_last_run["index"] == 1 assert new_last_run["issue_ids"] == ["dummy_uid"] assert new_last_run["search_string"] == "" def test_fetch_incidents_asm_issue_index_increment_with_same_last_seen_timestamp(mock_client, requests_mock, mocker): """ Test fetch_incidents with same last seen timestamp. Given: - Same last seen timestamp in the response When: - Running fetch_incidents function Then: - Validate that fetch_incidents returns expected results """ from GoogleThreatIntelligenceASMIssues import fetch_incidents params = { "first_fetch": "2025-06-30T09:45:30.711Z", "max_fetch": "2", "project_id": "12345", "search_string": "", "mirror_direction": "Outgoing", "note_tag": "note", } mock_response = util_load_json("test_data/fetch_asm_issue_success_response.json") fetched_incidents = util_load_json( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_asm_issue_skip_duplicates.json") ) last_run = { "max_fetch": 2, "asm_latest_issue_last_seen": "2025-07-14T15:18:51.000Z", "index": 2, "issue_ids": ["dummy_uid_01", "dummy_uid_02"], } # Mock demisto.params() to return our test parameters mocker.patch.object(demisto, "params", return_value=params) requests_mock.get( f"{BASE_URL}/{ENDPOINTS['issue_list'].format('last_seen_after:2025-07-14T15:18:51.000Z')}", json=mock_response ) issue_ids = [issue["uid"] for issue in mock_response["result"]["hits"]] # Call fetch_incidents incidents, new_last_run = fetch_incidents(client=mock_client, last_run=last_run, params=params) # Assert expected results assert incidents == fetched_incidents assert new_last_run["max_fetch"] == 2 assert new_last_run["asm_latest_issue_last_seen"] == "2025-07-14T15:18:51.000Z" assert new_last_run["index"] == 3 assert new_last_run["issue_ids"] == issue_ids assert new_last_run["search_string"] == "" def test_fetch_incidents_asm_issue_issue_data_less_than_max_fetch(mock_client, requests_mock, mocker): """ Test fetch_incidents with issue data less than max fetch. Given: - Issue data less than max fetch When: - Running fetch_incidents function Then: - Validate that fetch_incidents returns expected results """ from GoogleThreatIntelligenceASMIssues import fetch_incidents params = { "first_fetch": "2025-06-30T09:45:30.711Z", "max_fetch": "3", "project_id": "12345", "search_string": "", "mirror_direction": "Outgoing", "note_tag": "note", } mock_response = util_load_json("test_data/fetch_asm_issue_success_response.json") fetched_incidents = util_load_json( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_asm_issue_skip_duplicates.json") ) last_run = { "max_fetch": 3, "asm_latest_issue_last_seen": "2025-07-14T15:18:51.000Z", "index": 2, "issue_ids": ["dummy_uid_01", "dummy_uid_02"], } # Mock demisto.params() to return our test parameters mocker.patch.object(demisto, "params", return_value=params) requests_mock.get( f"{BASE_URL}/{ENDPOINTS['issue_list'].format('last_seen_after:2025-07-14T15:18:51.000Z')}", json=mock_response ) issue_ids = [issue["uid"] for issue in mock_response["result"]["hits"]] # Call fetch_incidents incidents, new_last_run = fetch_incidents(client=mock_client, last_run=last_run, params=params) # Assert expected results assert incidents == fetched_incidents assert new_last_run["max_fetch"] == 3 assert new_last_run["asm_latest_issue_last_seen"] == "2025-07-14T15:18:52.000Z" assert new_last_run["index"] == 1 assert new_last_run["issue_ids"] == issue_ids assert new_last_run["search_string"] == "" def test_fetch_incidents_asm_issue_index_limit_reached_timestamp_bump(mock_client, requests_mock, mocker): """ Test fetch_incidents when index limit is reached and timestamp should be bumped. Given: - Index at maximum limit (MAX_ISSUE_SIZE // max_fetch) - Same timestamp in response as last_run When: - Running fetch_incidents function Then: - Validate that timestamp is bumped by 1 second - Validate that index is reset to 1 """ from GoogleThreatIntelligenceASMIssues import fetch_incidents params = { "first_fetch": "2025-06-30T09:45:30.711Z", "max_fetch": 100, "project_id": "12345", "search_string": "", "mirror_direction": "Outgoing", "note_tag": "note", } mock_response = util_load_json("test_data/fetch_asm_issue_success_response.json") fetched_incidents = util_load_json( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_asm_issue_skip_duplicates.json") ) # Set index to max_index (1000 // 100 = 10) last_run = { "max_fetch": 100, "asm_latest_issue_last_seen": "2025-07-14T15:18:51.000Z", "index": 10, "issue_ids": ["dummy_uid_01", "dummy_uid_02"], } # Mock demisto.params() to return our test parameters mocker.patch.object(demisto, "params", return_value=params) requests_mock.get( f"{BASE_URL}/{ENDPOINTS['issue_list'].format('last_seen_after:2025-07-14T15:18:51.000Z')}", json=mock_response ) issue_ids = [issue["uid"] for issue in mock_response["result"]["hits"]] # Call fetch_incidents incidents, new_last_run = fetch_incidents(client=mock_client, last_run=last_run, params=params) assert incidents == fetched_incidents assert new_last_run["max_fetch"] == 100 assert new_last_run["asm_latest_issue_last_seen"] == "2025-07-14T15:18:52.000Z" assert new_last_run["index"] == 1 assert new_last_run["issue_ids"] == issue_ids assert new_last_run["search_string"] == "" @pytest.mark.parametrize( "old_max_fetch, new_max_fetch, old_index, expected_new_index", [ # max_fetch increased scenarios (2, 3, 2, 1), # (2-1)*2=2, 2//3+1=2 # max_fetch decreased scenarios (3, 2, 2, 2), # (2-1)*3=3, 3//2+1=2 ], ) def test_fetch_incidents_asm_issue_max_fetch_change_index_recalculation( mock_client, requests_mock, mocker, old_max_fetch, new_max_fetch, old_index, expected_new_index ): """ Test fetch_incidents when max_fetch parameter changes and index is recalculated. Given: - Different max_fetch values (increased/decreased) - Existing last_run with old max_fetch and index When: - Running fetch_incidents function with new max_fetch Then: - Validate that index is recalculated correctly - Formula: new_index = ((old_index - 1) * old_max_fetch) // new_max_fetch + 1 """ from GoogleThreatIntelligenceASMIssues import fetch_incidents params = { "first_fetch": "2025-06-30T09:45:30.711Z", "max_fetch": new_max_fetch, "project_id": "12345", "search_string": "collection:google", "mirror_direction": "Outgoing", "note_tag": "note", } mock_response = util_load_json("test_data/fetch_asm_issue_success_response.json") fetched_incidents = util_load_json( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_asm_issue_success_incidents.json") ) # Set up last_run with old max_fetch and index last_run = { "max_fetch": old_max_fetch, "asm_latest_issue_last_seen": "2025-07-14T15:18:51.000Z", "index": old_index, "search_string": "collection:google", } # Mock demisto.params() to return our test parameters mocker.patch.object(demisto, "params", return_value=params) requests_mock.get( f"{BASE_URL}/{ENDPOINTS['issue_list'].format('collection:google last_seen_after:2025-07-14T15:18:51.000Z')}", json=mock_response, ) # Call fetch_incidents incidents, new_last_run = fetch_incidents(client=mock_client, last_run=last_run, params=params) # Assert expected results assert incidents == fetched_incidents assert new_last_run["max_fetch"] == new_max_fetch assert new_last_run["asm_latest_issue_last_seen"] == "2025-07-14T15:18:51.000Z" assert new_last_run["index"] == expected_new_index + 1 assert new_last_run["search_string"] == "collection:google" @pytest.mark.parametrize( "old_search_string, new_search_string, old_index, expected_new_index, old_max_fetch, new_max_fetch", [ ("collection:google severity_gte:2", "collection:google severity_gte:3", 3, 1, 2, 2), ("collection:google severity_gte:2", "collection:google severity_gte:3", 3, 1, 2, 3), ], ) def test_fetch_incidents_when_search_string_changes( mock_client, requests_mock, mocker, old_search_string, new_search_string, old_index, expected_new_index, old_max_fetch, new_max_fetch, ): """ Test fetch_incidents when search string changes. Given: - Different search strings - Existing last_run with old search string When: - Running fetch_incidents function with new search string Then: - Validate that index is reset to 1 """ from GoogleThreatIntelligenceASMIssues import fetch_incidents params = { "first_fetch": "2025-06-30T09:45:30.711Z", "max_fetch": new_max_fetch, "project_id": "12345", "search_string": new_search_string, "mirror_direction": "Outgoing", "note_tag": "note", } mock_response = util_load_json("test_data/fetch_asm_issue_success_response.json") fetched_incidents = util_load_json( os.path.join(os.path.dirname(os.path.realpath(__file__)), "test_data/fetch_asm_issue_success_incidents.json") ) # Set up last_run with old search string last_run = { "max_fetch": old_max_fetch, "asm_latest_issue_last_seen": "2025-07-09T15:18:51.000Z", "index": old_index, "search_string": old_search_string, } # Mock demisto.params() to return our test parameters mocker.patch.object(demisto, "params", return_value=params) requests_mock.get( f"{BASE_URL}/{ENDPOINTS['issue_list'].format(new_search_string + ' last_seen_after:2025-07-09T15:18:51.000Z')}", json=mock_response, ) # Call fetch_incidents incidents, new_last_run = fetch_incidents(client=mock_client, last_run=last_run, params=params) # Assert expected results assert incidents == fetched_incidents assert new_last_run["max_fetch"] == new_max_fetch assert new_last_run["asm_latest_issue_last_seen"] == "2025-07-14T15:18:51.000Z" assert new_last_run["index"] == expected_new_index assert new_last_run["search_string"] == new_search_string def test_update_remote_system_command_incident_closed_status_update(mocker, requests_mock, mock_client): """ Given: - Valid arguments with incident status DONE (closed) When: - Running update_remote_system_command Then: - Issue status should be updated to 'closed' via POST request. """ from GoogleThreatIntelligenceASMIssues import update_remote_system_command # Mock UpdateRemoteSystemArgs mock_args = mocker.Mock() mock_args.remote_incident_id = "remote_123" mock_args.data = {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"} mock_args.inc_status = 2 # IncidentStatus.DONE mock_args.delta = {} mock_args.incident_changed = True mock_args.entries = [] mocker.patch("GoogleThreatIntelligenceASMIssues.UpdateRemoteSystemArgs", return_value=mock_args) # Mock API endpoint for status update requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_status_update'].format('issue_456')}", json={"success": True}) args = { "remote_incident_id": "remote_123", "data": {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"}, "inc_status": 2, # IncidentStatus.DONE "delta": {}, "incident_changed": True, "entries": [], } result = update_remote_system_command(mock_client, args) # Verify the result assert result == "remote_123" # Verify the POST request was made with the correct payload history = requests_mock.request_history assert len(history) == 1 assert history[0].method == "POST" expected_update = {"status": "closed"} assert history[0].json() == expected_update def test_update_remote_system_command_success(mocker, requests_mock, mock_client): """ Given: - Valid arguments with incident status DONE, tags in delta, and new entries When: - Running update_remote_system_command Then: - Issue status should be updated to 'closed' - New tags should be added to the issue - New notes should be added to the issue """ from GoogleThreatIntelligenceASMIssues import update_remote_system_command # Mock UpdateRemoteSystemArgs mock_args = mocker.Mock() mock_args.remote_incident_id = "remote_123" mock_args.data = {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"} mock_args.inc_status = 2 # IncidentStatus.DONE mock_args.delta = {"tags": ["urgent"]} mock_args.incident_changed = True mock_args.entries = [{"id": "entry_1", "type": "note", "contents": "Investigation completed", "user": "analyst1"}] mocker.patch("GoogleThreatIntelligenceASMIssues.UpdateRemoteSystemArgs", return_value=mock_args) # Mock API endpoints requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_status_update'].format('issue_456')}", json={"success": True}) requests_mock.get(f"{BASE_URL}/{ENDPOINTS['issue_tags'].format('issue_456')}", json={"result": []}) requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_tags'].format('issue_456')}", json={"success": True}) requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_update_notes'].format('issue_456')}", json={"success": True}) args = { "remote_incident_id": "remote_123", "data": {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"}, "inc_status": 2, # IncidentStatus.DONE "delta": {"tags": ["urgent"]}, "incident_changed": True, "entries": [{"id": "entry_1", "type": "note", "contents": "Investigation completed", "user": "analyst1"}], } result = update_remote_system_command(mock_client, args) # Verify the result assert result == "remote_123" # Verify all API requests were made history = requests_mock.request_history assert len(history) == 4 # status update + get tags + tag updates + note update # Verify status update request status_request = history[0] assert status_request.method == "POST" assert "status" in status_request.url assert status_request.json() == {"status": "closed"} # Verify tags get request tags_get_request = history[1] assert tags_get_request.method == "GET" assert "tags" in tags_get_request.url # Verify tag update requests tag_update_1 = history[2] assert tag_update_1.method == "POST" assert "tags" in tag_update_1.url assert tag_update_1.json() == {"tag_name": "urgent"} # Verify note update request note_request = history[3] assert note_request.method == "POST" assert "notes" in note_request.url expected_note = "[Mirrored From XSOAR] | Incident ID: xsoar_incident_123 | Note: Investigation completed | Added By: analyst1" assert note_request.json() == {"note_text": expected_note} assert note_request.json() == {"note_text": expected_note} def test_update_remote_system_command_note_exceeds_limit(mocker, requests_mock, mock_client): """ Given: - Valid arguments with new entries containing note content exceeding MAX_OUTGOING_NOTE_LIMIT When: - Running update_remote_system_command Then: - Note should be skipped and not sent to API - Info message should be logged about skipping the note """ from GoogleThreatIntelligenceASMIssues import update_remote_system_command, MAX_OUTGOING_NOTE_LIMIT # Create note content that exceeds the limit long_note_content = "x" * (MAX_OUTGOING_NOTE_LIMIT + 100) # Mock UpdateRemoteSystemArgs mock_args = mocker.Mock() mock_args.remote_incident_id = "remote_123" mock_args.data = {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"} mock_args.inc_status = 1 # IncidentStatus.ACTIVE mock_args.delta = {} mock_args.incident_changed = True mock_args.entries = [{"id": "entry_1", "type": "note", "contents": long_note_content, "user": "analyst1"}] mocker.patch("GoogleThreatIntelligenceASMIssues.UpdateRemoteSystemArgs", return_value=mock_args) # Mock demisto.info to capture the log message mock_info = mocker.patch("GoogleThreatIntelligenceASMIssues.demisto.info") # No API endpoints should be called for notes since it exceeds limit requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_update_notes'].format('issue_456')}", json={"success": True}) requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_status_update'].format('issue_456')}", json={"success": True}) args = { "remote_incident_id": "remote_123", "data": {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"}, "inc_status": 1, # IncidentStatus.ACTIVE "delta": {}, "incident_changed": True, "entries": [{"id": "entry_1", "type": "note", "contents": long_note_content, "user": "analyst1"}], } result = update_remote_system_command(mock_client, args) # Verify the result assert result == "remote_123" # Verify no API requests were made (status update, no note update) history = requests_mock.request_history assert len(history) == 1 # only status update # Verify info message was logged about skipping the note mock_info.assert_called_once_with( "Skipping outgoing mirroring for issue note with XSOAR Incident ID:xsoar_incident_123, " "because the note length exceeds 8000 characters." ) def test_update_remote_system_command_status_update_only_no_delta(mocker, requests_mock, mock_client): """ Given: - Valid arguments with incident status ACTIVE and empty delta When: - Running update_remote_system_command Then: - Only status should be updated to 'open_in_progress' - No tags or notes should be processed """ from GoogleThreatIntelligenceASMIssues import update_remote_system_command # Mock UpdateRemoteSystemArgs mock_args = mocker.Mock() mock_args.remote_incident_id = "remote_123" mock_args.data = {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"} mock_args.inc_status = 1 # IncidentStatus.ACTIVE mock_args.delta = {} mock_args.incident_changed = True mock_args.entries = [] mocker.patch("GoogleThreatIntelligenceASMIssues.UpdateRemoteSystemArgs", return_value=mock_args) # Mock API endpoint for status update only requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_status_update'].format('issue_456')}", json={"success": True}) args = { "remote_incident_id": "remote_123", "data": {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"}, "inc_status": 1, # IncidentStatus.ACTIVE "delta": {}, "incident_changed": True, "entries": [], } result = update_remote_system_command(mock_client, args) # Verify the result assert result == "remote_123" # Verify only one API request was made (status update only) history = requests_mock.request_history assert len(history) == 1 # Verify status update request status_request = history[0] assert status_request.method == "POST" assert "status" in status_request.url assert status_request.json() == {"status": "open_in_progress"} def test_update_remote_system_command_tags_update_mixed_existing_new(mocker, requests_mock, mock_client): """ Given: - Valid arguments with two tags in delta: one new tag and one existing tag When: - Running update_remote_system_command Then: - Only the new tag should be added via API call - The existing tag should be skipped (no API call for it) - Debug message should be logged for the existing tag """ from GoogleThreatIntelligenceASMIssues import update_remote_system_command # Mock UpdateRemoteSystemArgs mock_args = mocker.Mock() mock_args.remote_incident_id = "remote_123" mock_args.data = {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"} mock_args.inc_status = 1 # IncidentStatus.ACTIVE mock_args.delta = {"tags": ["urgent", "existing"]} mock_args.incident_changed = True mock_args.entries = [] mocker.patch("GoogleThreatIntelligenceASMIssues.UpdateRemoteSystemArgs", return_value=mock_args) # Mock demisto.debug to capture the log message for existing tag mock_debug = mocker.patch("GoogleThreatIntelligenceASMIssues.demisto.debug") # Mock API endpoints # Return existing tags where "existing" tag already exists (case-insensitive) requests_mock.get(f"{BASE_URL}/{ENDPOINTS['issue_tags'].format('issue_456')}", json={"result": ["Existing", "Critical"]}) requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_tags'].format('issue_456')}", json={"success": True}) requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_status_update'].format('issue_456')}", json={"success": True}) args = { "remote_incident_id": "remote_123", "data": {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"}, "inc_status": 1, # IncidentStatus.ACTIVE "delta": {"tags": ["urgent", "existing"]}, "incident_changed": True, "entries": [], } result = update_remote_system_command(mock_client, args) # Verify the result assert result == "remote_123" # Verify API requests: get tags + only one tag update (for "urgent") history = requests_mock.request_history assert len(history) == 2 # (get tags, update tags) # Verify tags get request tags_get_request = history[0] assert tags_get_request.method == "GET" assert "tags" in tags_get_request.url # Verify only one tag update request (for "urgent" tag) tag_update_request = history[1] assert tag_update_request.method == "POST" assert "tags" in tag_update_request.url assert tag_update_request.json() == {"tag_name": "urgent"} # Verify debug message was logged for existing tag mock_debug.assert_any_call("Tag existing already exists for issue issue_456") def test_update_remote_system_command_incident_reopen(mocker, requests_mock, mock_client): """ Given: - Valid arguments with incident status ACTIVE and incident reopen. When: - Running update_remote_system_command Then: - Only status should be updated to 'open_in_progress' - No tags or notes should be processed """ from GoogleThreatIntelligenceASMIssues import update_remote_system_command # Mock UpdateRemoteSystemArgs mock_args = mocker.Mock() mock_args.remote_incident_id = "remote_123" mock_args.data = {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"} mock_args.inc_status = 1 # IncidentStatus.ACTIVE mock_args.delta = {"closingUserId": "", "runStatus": ""} mock_args.incident_changed = True mock_args.entries = [] mocker.patch("GoogleThreatIntelligenceASMIssues.UpdateRemoteSystemArgs", return_value=mock_args) # Mock API endpoint for status update only requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_status_update'].format('issue_456')}", json={"success": True}) args = { "remote_incident_id": "remote_123", "data": {"gtiasmissueuid": "issue_456", "id": "xsoar_incident_123"}, "inc_status": 1, # IncidentStatus.ACTIVE "delta": {"closingUserId": "", "runStatus": ""}, "incident_changed": True, "entries": [], } result = update_remote_system_command(mock_client, args) # Verify the result assert result == "remote_123" # Verify only one API request was made (status update only) history = requests_mock.request_history assert len(history) == 1 # Verify status update request status_request = history[0] assert status_request.method == "POST" assert "status" in status_request.url assert status_request.json() == {"status": "open_in_progress"} def test_update_remote_system_command_no_mirror_issue_id(mocker, requests_mock, mock_client): """ Given: - Arguments with missing mirror Issue ID When: - Running update_remote_system_command Then: - No update should be made and no API calls should be sent. """ from GoogleThreatIntelligenceASMIssues import update_remote_system_command # Mock UpdateRemoteSystemArgs mock_args = mocker.Mock() mock_args.remote_incident_id = "remote_123" mock_args.data = {} # No issueID mock_args.inc_status = 2 mock_args.delta = {"tags": ["test"]} mock_args.incident_changed = True mocker.patch("GoogleThreatIntelligenceASMIssues.UpdateRemoteSystemArgs", return_value=mock_args) args = { "remote_incident_id": "remote_123", "data": {}, # No issueID "inc_status": 2, "delta": {"tags": ["test"]}, "incident_changed": True, } result = update_remote_system_command(mock_client, args) # Verify the result assert result == "remote_123" # Verify no API calls were made assert requests_mock.call_count == 0 def test_update_remote_system_command_closing_notes_mirroring(mocker, requests_mock, mock_client): """ Given: - Incident is DONE and delta contains 'closingUserId' When: - Running update_remote_system_command Then: - Status should be updated to 'closed' - A closing note should be sent with the expected payload """ from GoogleThreatIntelligenceASMIssues import update_remote_system_command # Mock UpdateRemoteSystemArgs to simulate a closed incident with closingUserId mock_args = mocker.Mock( remote_incident_id="remote_123", data={ "gtiasmissueuid": "issue_456", "id": "xsoar_incident_123", "closeNotes": "All good", "closeReason": "Resolved", "closingUserId": "user_789", }, inc_status=2, # IncidentStatus.DONE delta={"closingUserId": "user_789"}, incident_changed=True, entries=[], ) mocker.patch("GoogleThreatIntelligenceASMIssues.UpdateRemoteSystemArgs", return_value=mock_args) # Mock API endpoints requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_status_update'].format('issue_456')}", json={"success": True}) requests_mock.post(f"{BASE_URL}/{ENDPOINTS['issue_update_notes'].format('issue_456')}", json={"success": True}) # Execute result = update_remote_system_command( mock_client, { "remote_incident_id": "remote_123", "data": mock_args.data, "inc_status": 2, "delta": {"closingUserId": "user_789"}, "incident_changed": True, "entries": [], }, ) # Assert assert result == "remote_123" history = requests_mock.request_history assert history[0].json() == {"status": "closed"} expected_note = ( "[Mirrored From XSOAR] | Incident ID: xsoar_incident_123 | Close Reason: Resolved |" "Closed By: user_789 | Close Notes: All good" ) assert history[1].json() == {"note_text": expected_note}