GoogleThreatIntelligenceASMIssues

This integration allows the creation of incidents based on ASM Issues from Google Threat Intelligence.

Data Enrichment & Threat Intelligence · GoogleThreatIntelligence

Details

IDGoogleThreatIntelligenceASMIssues
ProviderGoogle
CategoryData Enrichment & Threat Intelligence
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix 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:

How to get api key in GoogleThreatIntelligence

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 google 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 google 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 incidents
  • max_fetch — Max Fetch
  • first_fetch — First Fetch Time
  • mirror_direction — Mirroring Direction
  • note_tag — Mirror tag for notes
  • project_id — Project ID
  • search_string — Search String
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval

Commands (3)

  • gti-asm-issue-get

    Get a particular ASM Issue by ID.

  • gti-asm-issue-list

    Search the ASM Issues with provided filter arguments.

  • gti-asm-issue-status-update

    Update 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}