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 collections.abc import Callable from urllib.parse import unquote from CommonServerPython import * import demistomock as demisto # noqa: F401 """ An integration module for the Google Threat Intelligence ASM Issues API. API Documentation: https://gtidocs.virustotal.com/reference/get_search-issues-search-string """ COMMAND_PREFIX = "gti" BASE_URL = "https://www.virustotal.com/api/v3" ASM_INCIDENT_LINK = "https://asm.advantage.mandiant.com/issues/{}" OK_CODES = (200, 401) STATUS_CODE_TO_RETRY = [429, *(status_code for status_code in requests.status_codes._codes if status_code >= 500)] # type: ignore MAX_RETRIES = 4 BACKOFF_FACTOR = 7.5 MAX_FETCH = 200 DEFAULT_API_MAX_FETCH = 50 DEFAULT_MAX_FETCH = 100 MAX_ISSUE_SIZE = 1000 MAX_OUTGOING_NOTE_LIMIT = 50000 DEFAULT_FETCH_TIME = "1 days" OUTPUT_PREFIX = {"ISSUE_LIST": "GoogleThreatIntelligenceASMIssues.Issues"} MIRROR_DIRECTION = {"Outgoing": "Out"} ASM_ISSUE_INCIDENT_STATUS_MAPPING = { 1: "open_in_progress", # incident status: active 2: "closed", # incident status: done } DATE_TIME_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ" DEFAULT_FETCH_TIME = "1 days" OUTPUT_PREFIX = {"ISSUE_LIST": "GoogleThreatIntelligenceASMIssues.Issues"} ISSUE_TO_INCIDENT_SEVERITY = { 1: 4, 2: 3, 3: 2, 4: 1, 5: 0.5, } MESSAGES = { "NO_RECORDS_FOUND": "No {} were found for the given argument(s).", "NO_RECORD_FOUND": "No {} was found for the given argument(s).", "REQUIRED_ARGUMENT": "Missing argument {}.", "NO_STATUS_UPDATED": "No {} status was updated.", } ERROR_MESSAGES = { "INVALID_MAX_FETCH": "'{}' is invalid 'max_fetch' value. Max fetch for ASM Issues should be between 1 and 200.", "INVALID_PAGE_SIZE": "'{}' is an invalid value for 'page_size'. Value must be between 1 and 1000.", "INVALID_OBJECT": "Failed to parse {} object from response: {}", "UNAUTHORIZED_REQUEST": "{} Unauthorized request: Invalid API key provided {}.", "INVALID_ARGUMENT": "'{}' is an invalid value for '{}'. Value must be in {}.", } ASM_ISSUE_STATUS_LIST = [ "open", "triaged", "in progress", "closed", "mitigated", "resolved", "duplicate", "out of scope", "false positive", "risk accepted", "benign", "unable to reproduce", "track externally", ] ASM_ISSUE_STATUS_HUMAN_READABLE = [ "Open", "Triaged", "In Progress", "Closed", "Mitigated", "Resolved", "Duplicate", "Out of Scope", "False Positive", "Risk Accepted", "Benign", "Unable to Reproduce", "Track Externally", ] ASM_ISSUE_STATUS_API_MAPPING = { "open": "open_new", "triaged": "open_triaged", "in progress": "open_in_progress", "closed": "closed", "mitigated": "closed_mitigated", "resolved": "closed_resolved", "duplicate": "closed_duplicate", "out of scope": "closed_out_of_scope", "false positive": "closed_false_positive", "risk accepted": "closed_risk_accepted", "benign": "closed_benign", "unable to reproduce": "closed_no_repro", "track externally": "closed_tracked_externally", } ASM_ISSUE_HR_STATUS_MAPPING = { "open_new": "Open", "open_triaged": "Triaged", "open_in_progress": "In Progress", "closed": "Closed", "closed_mitigated": "Mitigated", "closed_resolved": "Resolved", "closed_duplicate": "Duplicate", "closed_out_of_scope": "Out of Scope", "closed_false_positive": "False Positive", "closed_risk_accepted": "Risk Accepted", "closed_benign": "Benign", "closed_no_repro": "Unable to Reproduce", "closed_tracked_externally": "Track Externally", } ENDPOINTS = { "issue_list": "asm/search/issues/{}", "issue_get": "asm/issues/{}", "issue_status_update": "asm/issues/{}/status", "issue_tags": "asm/tags/issue/{}", "issue_update_notes": "asm/notes/issue/{}", } class Client(BaseClient): """Client for Google Threat Intelligence ASM Issues API.""" def __init__(self, verify_certificate: bool, proxy: bool, api_key: str, project_id: str): self.project_id = project_id super().__init__( BASE_URL, verify=verify_certificate, proxy=proxy, headers={ "x-apikey": api_key, "Accept": "application/json", "Content-Type": "application/json", "PROJECT-ID": project_id, }, ) def http_request( self, method: str, url_suffix: str = "", params: Dict[str, Any] | None = None, data: Dict[str, Any] | None = None, json_data: Dict[str, Any] | None = None, response_type: str = "response", headers: Dict[str, Any] | None = None, **kwargs, ): """ Makes an HTTP request to the server. Args: method (str): The HTTP method (e.g., GET, POST, PUT, DELETE). url_suffix (str): The URL suffix to be appended to the base URL. Defaults to an empty string. params (dict): Query parameters to be appended to the URL. Defaults to None. data (object): Data to be sent in the request body. Defaults to None. json_data (dict): JSON data to be sent in the request body. Defaults to None. response_type (str): The expected response type. Defaults to None. **kwargs: Additional keyword arguments. Returns: object: The response object or None. """ # if not provided ASM Project ID for commands if not headers: headers = self._headers log_header = {**headers, "x-apikey": "***********"} # type: ignore demisto.debug( f"Making API request at {method} {url_suffix} with headers:{log_header}," f"params:{params} and body:{data or json_data}" ) # Make the HTTP request using the _http_request method, passing the necessary parameters. res = self._http_request( method=method, url_suffix=url_suffix, headers=headers, data=data, json_data=json_data, params=params, retries=MAX_RETRIES, status_list_to_retry=STATUS_CODE_TO_RETRY, ok_codes=OK_CODES, backoff_factor=BACKOFF_FACTOR, resp_type="response", raise_on_status=True, **kwargs, ) if res.status_code in [401]: try: err_msg = ERROR_MESSAGES["UNAUTHORIZED_REQUEST"].format(res.status_code, str(res.json())) except ValueError: err_msg = ERROR_MESSAGES["UNAUTHORIZED_REQUEST"].format(res.status_code, str(res)) raise DemistoException(err_msg) # Parse successful response based on requested type try: if response_type == "json": return res.json() elif response_type == "response": return res else: return res # Default to response object except ValueError as e: raise DemistoException( ERROR_MESSAGES["INVALID_OBJECT"].format(response_type, res.content), e, res, ) def get_issue_list(self, query_params: dict, search_string: str, project_id: str | None = None, response_type: str = "json"): """ See Also: https://gtidocs.virustotal.com/reference/get_search-issues-search-string """ headers = None if project_id: headers = self._headers.copy() headers.update({"PROJECT-ID": project_id}) response = self.http_request( method="GET", url_suffix=ENDPOINTS["issue_list"].format(search_string), params=query_params, response_type=response_type, headers=headers, ) if not response.get("success") or not response.get("result"): raise ValueError( f"Failed to retrieve ASM issues from Google Threat Intelligence API. Verify the search string and project ID" f" are correct, and try again. Provided Search string: '{search_string}', Project ID: '{project_id}'" ) return response def asm_issue_get(self, issue_id: str, project_id: str | None = None) -> dict: """ Get a particular ASM issue by ID. Args: issue_id(str): Issue ID. project_id(str): Project ID. Returns: dict: ASM issue. """ headers = None if project_id: headers = self._headers.copy() headers["PROJECT-ID"] = project_id return self.http_request( method="GET", url_suffix=ENDPOINTS["issue_get"].format(issue_id), response_type="json", headers=headers, ) def asm_issue_status_update(self, issue_id: str, payload: dict[str, Any], project_id: str | None = None) -> dict: """ Update the status of a particular ASM issue by ID. Args: issue_id(str): Issue ID. payload(dict): Payload to update the issue. project_id(str): Project ID. Returns: dict: ASM issue. """ headers = None if project_id: headers = self._headers.copy() headers["PROJECT-ID"] = project_id return self.http_request( method="POST", url_suffix=ENDPOINTS["issue_status_update"].format(issue_id), headers=headers, response_type="json", json_data=payload, ) def asm_issue_tags_update(self, issue_id: str, payload: dict[str, Any]) -> dict: """ Update the tags of a particular ASM issue by ID. Args: issue_id(str): Issue ID. tags(list): New tags of the issue. Returns: dict: ASM issue. """ return self.http_request( method="POST", url_suffix=ENDPOINTS["issue_tags"].format(issue_id), response_type="json", json_data=payload, ) def asm_issue_tags_get(self, issue_id: str) -> dict: """ Get the tags of a particular ASM issue by ID. Args: issue_id(str): Issue ID. Returns: dict: ASM issue. """ return self.http_request( method="GET", url_suffix=ENDPOINTS["issue_tags"].format(issue_id), response_type="json", ) def asm_issue_notes_update(self, issue_id: str, payload: dict[str, Any]) -> dict: """ Update the notes of a particular ASM issue by ID. Args: issue_id(str): Issue ID. notes(list): New notes of the issue. Returns: dict: ASM issue. """ return self.http_request( method="POST", url_suffix=ENDPOINTS["issue_update_notes"].format(issue_id), response_type="json", json_data=payload, ) # ASM Issues Helper functions def validate_argument(value, name) -> Any: """ Check if empty string is passed as value for argument and raise appropriate ValueError. Args: value: Value of the argument. name: Name of the argument. Returns: Any: Value of the argument. Raises: ValueError: If the value is empty string. """ if not value: raise ValueError(MESSAGES["REQUIRED_ARGUMENT"].format(name)) return value def trim_spaces_from_args(args): """ Trim spaces from values of the args dict. Args: args: Dict to trim spaces from Returns: dict: Dict with trimmed spaces from values """ for key, val in args.items(): if isinstance(val, str): args[key] = val.strip() return args def build_asm_issue_list_output(issue_data: list, hr_title: str) -> tuple: """Build human-readable output and context for ASM issues. Args: issue_data(list): List of issues from ASM issues API. hr_title(str): Human-readable title. Returns: tuple: (context, readable_output) """ hr_content = [] context = [] for issue in issue_data: context.append(remove_empty_elements(issue)) # Extract key fields for human-readable table matching your structure issue_summary = issue.get("summary", {}) issue_status = issue_summary.get("status", "") hr_content.append( { "Issue ID": issue.get("uid", ""), "Issue Name": issue.get("pretty_name", ""), "Issue Description": issue.get("description", ""), "Status": ASM_ISSUE_HR_STATUS_MAPPING.get(issue_status, issue_status), "Severity": issue_summary.get("severity", ""), "Entity Name": issue.get("entity_name", ""), "Entity uid": issue.get("entity_uid", ""), "Entity Type": issue.get("entity_type", ""), "Collection": issue.get("collection", ""), "Confidence": issue_summary.get("confidence", ""), "Last Seen": issue.get("last_seen", ""), "First Seen": issue.get("first_seen", ""), "Tags": issue.get("tags", ""), } ) headers = [ "Issue ID", "Issue Name", "Issue Description", "Status", "Severity", "Entity Name", "Entity uid", "Entity Type", "Collection", "Confidence", "Last Seen", "First Seen", "Tags", ] # Create human-readable output using tableToMarkdown readable_output = tableToMarkdown(hr_title, hr_content, headers=headers, removeNull=True) return context, readable_output def build_asm_issue_status_update_output(issue_data: dict) -> tuple: """ Build human-readable output and context for ASM issue status update. Args: issue_data(dict): Issue data obtained from ASM issue status update API. Returns: tuple: (context, readable_output) """ context = [] context.append(remove_empty_elements(issue_data)) hr_content = [] hr_content.append( { "Issue ID": issue_data.get("uid", ""), "Status": ASM_ISSUE_HR_STATUS_MAPPING.get(issue_data.get("result", ""), ""), } ) headers = ["Issue ID", "Status"] # Create human-readable output using tableToMarkdown readable_output = tableToMarkdown("ASM Issue Status Updated Successfully.", hr_content, headers=headers, removeNull=True) return context, readable_output def clean_search_string(search_string): """ Cleans the given search string by removing keyword:value pairs of timestamp and normalizing spacing. Args: search_string (str): The search string to be cleaned. Returns: str: The cleaned search string. Steps: 1.Add a space if a timestamp keyword is directly attached to the previous word. 2.Create a regex pattern to detect timestamp_keyword:value pairs. 3.Strip out all matched keyword–value pairs from the search string. 4.Clean up the spacing by collapsing multiple spaces and trimming edges. 5.log the final cleaned search string for debugging. """ timestamp_keys = ["last_seen_after", "last_seen_before", "first_seen_after"] # Step 1: Add a space if a timestamp keyword is directly attached to the previous word. for timestamp_key in timestamp_keys: search_string = re.sub(r"(?<![\s])(" + re.escape(timestamp_key) + r")\s*:", r" \1:", search_string) # Step 2: Create a regex pattern to detect timestamp_keyword:value pairs. pattern = r"\b(?:" + "|".join(re.escape(k) for k in timestamp_keys) + r")\s*:\s*[^ \n\t]+" # Step 3: Strip out all matched keyword–value pairs from the search string. cleaned = re.sub(pattern, "", search_string) # Step 4: Clean up the spacing by collapsing multiple spaces and trimming edges. cleaned = re.sub(r"\s+", " ", cleaned).strip() # Step 5: Log the final cleaned search string for debugging. demisto.debug(f"Cleaned search string for ASM: {cleaned}") return cleaned def bump_timestamp(timestamp_str: str) -> str: """Helper function to increment timestamp by 1 second""" timestamp_dt = datetime.strptime(timestamp_str, DATE_TIME_FORMAT) timestamp_dt += timedelta(seconds=1) # issue timestamps supports only support microsecond precision timestamp_dt = format_datetime(timestamp_dt) return timestamp_dt def format_datetime(dt: datetime) -> str: """Formats a datetime object into an ISO 8601 string with millisecond precision. This helper function converts a Python datetime object into a string format `YYYY-MM-DDTHH:MM:SS.sssZ`. It truncates microseconds to milliseconds to ensure compatibility. Args: dt (datetime): The datetime object to format. Returns: str: The formatted datetime string in ISO 8601 format with millisecond precision and a 'Z' for UTC. """ return dt.strftime(DATE_TIME_FORMAT)[:-4] + "Z" def get_mirroring(): """ Get the mirroring configuration parameters from the Demisto integration parameters. Returns: dict: A dictionary containing the mirroring configuration parameters. """ params = demisto.params() mirror_direction = params.get("mirror_direction", "None").strip() mirror_tags = params.get("note_tag", "").strip() return { "mirror_direction": MIRROR_DIRECTION.get(mirror_direction), "mirror_instance": demisto.integrationInstance(), "mirror_tags": mirror_tags, } def extract_tags(client: Client, issue_id: str) -> dict[str, str]: tags_dict = {} response = client.asm_issue_tags_get(issue_id) exisiting_tags = response.get("result", []) for tag in exisiting_tags: tags_dict[tag.lower()] = tag return tags_dict # ASM Issue command functions def test_module(client: Client) -> str: """ Test module for Google Threat Intelligence ASM Issues. Args: client: Client object. Returns: str: "ok" if connection with Google Threat Intelligence is successful. """ params = demisto.params() is_fetch = params.get("isFetch", False) if is_fetch: fetch_incidents(client=client, last_run={}, params=params, is_test=True) else: query_params = {"page_size": 1} search_string = "status_new:open" client.get_issue_list(query_params, search_string) # return ok is connection with Google Threat Intelligence is successful. return "ok" def gti_asm_issue_get_command(client: Client, args: dict) -> CommandResults: """ Get ASM issue details for the specified issue ID. Args: client(Client): Client object to use. args(dict): arguments obtained from demisto.args(). Returns: CommandResults: CommandResult object """ issue_id = validate_argument(args.get("issue_id"), "issue_id") project_id = args.get("project_id") if not project_id: project_id = client.project_id raw_response = client.asm_issue_get(issue_id=issue_id, project_id=project_id) issue_data = raw_response.get("result", {}) if not raw_response.get("success") or not issue_data: return CommandResults(readable_output=MESSAGES["NO_RECORD_FOUND"].format("ASM Issue")) if isinstance(issue_data, dict): issue_data = [issue_data] context, hr = build_asm_issue_list_output(issue_data, "ASM Issue") return CommandResults( outputs_prefix=OUTPUT_PREFIX["ISSUE_LIST"], outputs_key_field="uid", outputs=context, raw_response=raw_response, readable_output=hr, ) def gti_asm_issue_status_update_command(client: Client, args: dict) -> CommandResults: """ Update the status of a particular ASM issue by ID. Args: client(Client): Client object to use. args(dict): arguments obtained from demisto.args(). Returns: CommandResults: CommandResult object """ issue_id = validate_argument(args.get("issue_id"), "issue_id") project_id = args.get("project_id") status = validate_argument(args.get("status"), "status") if not project_id: project_id = client.project_id if status and status.lower() not in ASM_ISSUE_STATUS_LIST: raise ValueError(ERROR_MESSAGES["INVALID_ARGUMENT"].format(status, "status", ASM_ISSUE_STATUS_HUMAN_READABLE)) payload = {"status": ASM_ISSUE_STATUS_API_MAPPING.get(status.lower())} raw_response = client.asm_issue_status_update(project_id=project_id, issue_id=issue_id, payload=payload) if not raw_response.get("success"): return CommandResults(readable_output=MESSAGES["NO_STATUS_UPDATED"].format("ASM Issue")) raw_response["uid"] = issue_id context, hr = build_asm_issue_status_update_output(raw_response) return CommandResults( outputs_prefix=OUTPUT_PREFIX["ISSUE_LIST"], outputs_key_field="uid", outputs=context, raw_response=raw_response, readable_output=hr, ) def gti_asm_issue_list_command(client: Client, args: dict) -> CommandResults: """ List ASM issues for the specified filter parameters. Args: client(Client): Client object to use. args(dict): arguments obtained from demisto.args(). Returns: CommandResults: CommandResult object """ project_id = args.get("project_id") search_string = args.get("search_string", " ") page_size = arg_to_number(args.get("page_size", DEFAULT_API_MAX_FETCH), "page_size") if not project_id: project_id = client.project_id if page_size is not None and (page_size > MAX_ISSUE_SIZE or page_size < 1): raise ValueError(ERROR_MESSAGES["INVALID_PAGE_SIZE"].format(page_size, MAX_ISSUE_SIZE)) query_params = {"page_size": page_size} raw_response = client.get_issue_list(project_id=project_id, query_params=query_params, search_string=search_string) if not (raw_response.get("success") and raw_response.get("result", {}).get("hits", [])): return CommandResults(readable_output=MESSAGES["NO_RECORDS_FOUND"].format("ASM Issues")) issue_data = raw_response.get("result", {}).get("hits", []) context, hr = build_asm_issue_list_output(issue_data, "ASM Issues") return CommandResults( outputs_prefix=OUTPUT_PREFIX["ISSUE_LIST"], outputs_key_field="uid", outputs=context, raw_response=raw_response, readable_output=hr, ) def fetch_incidents( client: Client, last_run: dict, params: dict, is_test: bool = False ) -> tuple[List[Dict[str, Any]], Dict[str, Any]]: """ Fetch ASM issues as incidents from Google Threat Intelligence. Args: client (Client): Google Threat Intelligence client object. last_run (dict): Last run data containing timestamp and index. params (Dict[str, Any]): Fetch incidents parameters. is_test (bool): Whether this is a test run. Returns: List[Dict[str, Any]]: List of fetched incidents. Dict[str, Any]: Next run parameters. """ # Get parameters with guaranteed non-None fallbacks first_fetch_time = arg_to_datetime(params.get("first_fetch", DEFAULT_FETCH_TIME)) # type: ignore max_fetch = arg_to_number(params.get("max_fetch", DEFAULT_MAX_FETCH), "max_fetch") search_string = params.get("search_string", " ") # Validate max_fetch if max_fetch is not None and (max_fetch > MAX_FETCH or max_fetch < 1): raise ValueError(ERROR_MESSAGES["INVALID_MAX_FETCH"].format(max_fetch)) # Initialize variables issue_incidents: List[Dict[str, Any]] = [] next_run_params: Dict[str, Any] = {} # Initialize fetch params last_seen_after = last_run.get("asm_latest_issue_last_seen", format_datetime(first_fetch_time)) # type: ignore index = last_run.get("index", 1) seen_ids = last_run.get("issue_ids", []) last_max_fetch = last_run.get("max_fetch", max_fetch) last_search_string = last_run.get("search_string", search_string) # Handle parameter changes search_string_changed = last_search_string != search_string max_fetch_changed = last_max_fetch != max_fetch # Handle changes in search string or max_fetch if search_string_changed or max_fetch_changed: # Recalculate index or reset it based on the type of change if not search_string_changed and max_fetch_changed: # Only max_fetch changed - recalculate index to maintain position demisto.debug(f"max_fetch changed from {last_max_fetch} to {max_fetch}, recalculating index") items_processed = (index - 1) * last_max_fetch index = (items_processed // max_fetch) + 1 else: # Search string changed (with or without max_fetch) - reset index demisto.debug(f"Search string changed from '{last_search_string}' to '{search_string}', resetting index to 1") index = 1 demisto.debug(f"Starting fetch with last_seen_after={last_seen_after}, index={index}, seen_ids count={len(seen_ids)}") # Handle index limit reached - increment timestamp by 1 second and Max index steps to avoid infinite loop max_index = MAX_ISSUE_SIZE // max_fetch # type: ignore # Calculate limit for pagination api_limit = min(max_fetch * index, MAX_ISSUE_SIZE) # API limit is 1000 # decode the search string and remove all the time params from search string decoded_search_string = unquote(search_string) cleaned_search_string = clean_search_string(decoded_search_string) # Add last seen time to search string final_search_string = f"{cleaned_search_string} last_seen_after:{last_seen_after}".strip() query_params = {"page_size": api_limit} demisto.debug(f"API call with search_string='{final_search_string}', page_size={api_limit}, index={index}") issue_response = client.get_issue_list(query_params=query_params, search_string=final_search_string) if is_test: return [], {} # Parse response issue_data = issue_response.get("result", {}).get("hits", []) demisto.debug(f"API returned {len(issue_data)} issues") found_issue_ids = [] duplicate_issue_ids = [] for issue in issue_data: issue_id = issue.get("uid") # Skip duplicates and invalid issues with no id if not issue_id or issue_id in seen_ids: if issue_id: duplicate_issue_ids.append(issue_id) continue # Update mirror params mirror_params = get_mirroring() mirror_params.update({"mirror_id": issue_id}) issue.update(mirror_params) issue["incident_link"] = ASM_INCIDENT_LINK.format(issue_id) issue = remove_empty_elements(issue) issue_incidents.append( { "name": issue.get("pretty_name", ""), "occurred": issue.get("first_seen", ""), "details": json.dumps(issue), "rawJSON": json.dumps(issue), "severity": ISSUE_TO_INCIDENT_SEVERITY.get(issue.get("summary", {}).get("severity", 0)), } ) found_issue_ids.append(issue_id) demisto.debug(f"Found {len(duplicate_issue_ids)} duplicates, found new issues IDs size: {len(found_issue_ids)}") demisto.debug(f"new issue IDs: {found_issue_ids}") new_last_timestamp = issue_data[-1].get("last_seen") if issue_data else last_seen_after new_seen_ids = seen_ids + found_issue_ids # if issue data is not found, return empty list and next run params if not issue_data: return [], last_run if new_last_timestamp == last_seen_after: if len(issue_data) < api_limit or index >= max_index: # Bump timestamp in either case: # 1. Partial response (< api_limit) = end of data for this timestamp # 2. Index limit reached (>= max_index) = prevent infinite loop new_last_timestamp = bump_timestamp(last_seen_after) new_index = 1 else: # Continue with same timestamp, increment index new_index = index + 1 else: # Different timestamp - use new timestamp as-is new_index = 1 next_run_params = { "asm_latest_issue_last_seen": new_last_timestamp, "index": new_index, "issue_ids": new_seen_ids, "max_fetch": max_fetch, "search_string": search_string, } demisto.debug(f"Returning {len(issue_incidents)} incidents, next_run: {next_run_params}") return issue_incidents, next_run_params # Mirroring Commands def update_remote_system_command(client: Client, args: Dict) -> str: """ Update a remote ASM issues based on changes in the XSOAR incident. Args: client (Client): An instance of the Client class. args (Dict): A dictionary containing the arguments required for updating the remote system. Returns: str: The ID of the updated remote alert. """ parsed_args = UpdateRemoteSystemArgs(args) remote_issue_id = parsed_args.remote_incident_id mirror_issue_id = parsed_args.data.get("gtiasmissueuid", "") # Get XSOAR incident id xsoar_incident_id = parsed_args.data.get("id", "") incident_status = parsed_args.inc_status delta = parsed_args.delta or {} xsoar_tags = delta.get("tags") or [] incident_changed = parsed_args.incident_changed new_entries = parsed_args.entries demisto.debug(f"Incident changed: {incident_changed}") demisto.debug(f"Delta Information for incident: {delta}") demisto.debug(f"Mirroring update for DTM alert: {mirror_issue_id} (remote_id: {remote_issue_id})") demisto.debug(f"Incident status: {incident_status}, XSOAR tags: {xsoar_tags}") if not mirror_issue_id: demisto.debug("No mirror alert ID found for update") return remote_issue_id # Update external status when incident is closed or when incident is active with no changes or incident get reopen reopen_incident = False if delta and delta.get("closingUserId") == "" and delta.get("runStatus") == "": reopen_incident = True is_update_status = ( incident_status == IncidentStatus.DONE or (incident_status == IncidentStatus.ACTIVE and not delta) or reopen_incident ) if incident_changed and is_update_status: update_data = {"status": ASM_ISSUE_INCIDENT_STATUS_MAPPING[incident_status]} client.asm_issue_status_update(issue_id=mirror_issue_id, payload=update_data) # Update tags when tags exist in delta and incident is changed if incident_changed and xsoar_tags: # get issue tags list existing_issue_tags = extract_tags(client=client, issue_id=mirror_issue_id) for tag in xsoar_tags: if tag.lower() not in existing_issue_tags: update_data = {"tag_name": tag} client.asm_issue_tags_update(issue_id=mirror_issue_id, payload=update_data) else: demisto.debug(f"Tag {tag} already exists for issue {mirror_issue_id}") if new_entries: for entry in new_entries: entry_id = entry.get("id") demisto.debug(f'Sending the entry with ID: {entry_id} and Type: {entry.get("type")}') # Get note content and user entry_content = entry.get("contents", "") entry_user = entry.get("user", "dbot") or "dbot" note_text = ( f"[Mirrored From XSOAR] | Incident ID: {xsoar_incident_id} | Note: {entry_content} | Added By: {entry_user}" ) if len(note_text) > MAX_OUTGOING_NOTE_LIMIT: demisto.info( f"Skipping outgoing mirroring for issue note with XSOAR Incident ID:{xsoar_incident_id}, " "because the note length exceeds 8000 characters." ) else: # API request for adding notes payload = {"note_text": note_text} client.asm_issue_notes_update(issue_id=mirror_issue_id, payload=payload) # For Closing notes delta_keys = parsed_args.delta.keys() if "closingUserId" in delta_keys and parsed_args.incident_changed and parsed_args.inc_status == IncidentStatus.DONE: # Check if incident status is Done close_notes = parsed_args.data.get("closeNotes", "") close_reason = parsed_args.data.get("closeReason", "") close_user_id = parsed_args.data.get("closingUserId", "") closing_note = ( f"[Mirrored From XSOAR] | Incident ID: {xsoar_incident_id} | Close Reason: {close_reason} |" f"Closed By: {close_user_id} | Close Notes: {close_notes}" ) if len(closing_note) > MAX_OUTGOING_NOTE_LIMIT: demisto.info( f"Skipping outgoing mirroring for closing notes with XSOAR Incident ID {xsoar_incident_id}, " f"because the note length exceeds {MAX_OUTGOING_NOTE_LIMIT} characters." ) else: # API request for adding notes payload = {"note_text": closing_note} client.asm_issue_notes_update(issue_id=mirror_issue_id, payload=payload) return remote_issue_id def main(): params = demisto.params() params = trim_spaces_from_args(params) remove_nulls_from_dictionary(params) # get connectivity parameters api_key = str(dict_safe_get(params, ["credentials", "password"])).strip() verify_certificate = not argToBoolean(params.get("insecure", False)) proxy = argToBoolean(params.get("proxy", False)) project_id = params.get("project_id", "") command = demisto.command() demisto.debug(f"Command being called is {command}") commands: dict[str, Callable] = { "gti-asm-issue-get": gti_asm_issue_get_command, "gti-asm-issue-status-update": gti_asm_issue_status_update_command, "gti-asm-issue-list": gti_asm_issue_list_command, } try: result = None # Creates client client = Client(verify_certificate, proxy, api_key, project_id) # Get Command args args = demisto.args() if command == "test-module": result = test_module(client) elif command == "update-remote-system": result = update_remote_system_command(client, args) elif command == "fetch-incidents": last_run = demisto.getLastRun() incidents, next_run = fetch_incidents(client, last_run, params) demisto.setLastRun(next_run) demisto.incidents(incidents) elif command in commands: # remove nulls from dictionary and trim space from args args = trim_spaces_from_args(args) remove_nulls_from_dictionary(args) result = commands[command](client, args) else: raise NotImplementedError(f"Command {command} is not implemented") return_results(result) # Returns either str, CommandResults and a list of CommandResults # Log exceptions and return errors except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{str(e)}") if __name__ in ("__main__", "__builtin__", "builtins"): # pragma: no cover main()