CTM360_CyberBlindspot
Take action on incidents derived from CTM360 CBS threat intelligence that is directly linked to your organization.
Analytics & SIEM · CTM360
Details
| ID | CTM360_CyberBlindspot |
|---|---|
| Provider | CTM360 |
| Category | Analytics & SIEM |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Take action on incidents derived from CTM360 CBS threat intelligence that is directly linked to your organization.
This integration was integrated and tested with version 1.0.0 of CTM360_CyberBlindspot.
Configure CTM360 CyberBlindspot in Cortex
| Parameter | Description | Required |
|---|---|---|
| Incident Mirroring Direction | Choose the direction to mirror the incident: Incoming (from CyberBlindspot to Cortex XSOAR), Outgoing (from Cortex XSOAR to CyberBlindspot), or Incoming and Outgoing (from/to Cortex XSOAR and CyberBlindspot). | False |
| Module To Use | The module to use: Incidents, Compromised Cards, Breached Credentials, Malware Logs, Domain/Subdomain Infringements, Social Media Fraud, Gambling Sites, or Money Mules. | False |
| Retrieve Screenshots | False | |
First fetch (<number> <time unit>, e.g., 12 hours. Default is 7 days) |
The time the incidents should be fetched starting from. | False |
| API Key | The CTM360 CyberBlindspot API Key to use for fetching data. | True |
| Maximum Number of Incidents per Fetch | Default is 25. Maximum is 200. | True |
| Fetch incidents | False | |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False | |
| Incident type | False |
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
ctm360-cbs-incident-list
Get the list of incidents from CBS.
Base Command
ctm360-cbs-incident-list
Input
| Argument Name | Description | Required |
|---|---|---|
| dateFrom | Select “From” date to fetch incidents starting from it. | Optional |
| dateTo | Select “To” date to fetch incidents up to it. | Optional |
| maxHits | Set number of results to fetch. | Optional |
| order | Set the order of the results. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| CyberBlindspot.IncidentList | unknown | List of all CBS incidents. |
Command example
!ctm360-cbs-incident-list dateFrom="23-10-2023 07:00" dateTo="23-10-2023 23:00" order=asc maxHits=2
Context Example
{
"CyberBlindspot": {
"IncidentList": [
{
"CustomFields": {
"cbs_class": "Link",
"cbs_coa": "Member Side Action",
"cbs_status": "Member Feedback",
"cbs_subject": "2 customer credentials compromised (5d65815)",
"cbs_timestamp": 1698049692779,
"cbs_type": "Leaked Credential",
"cbs_updated_date": "2023-10-23T08:00:00+00:00"
},
"externalstatus": "Member Feedback",
"name": "New leaked_credential with severity High found",
"occurred": "2023-10-23T08:00:00+00:00",
"rawJson": "{\"name\": \"New leaked_credential with severity High found\", \"occurred\": \"2023-10-23T08:00:00+00:00\", \"type\": \"Leaked Credential\", \"externalstatus\": \"Member Feedback\", \"severity\": 3, \"CustomFields\": {\"cbs_status\": \"Member Feedback\", \"cbs_subject\": \"2 customer credentials compromised (5d65815)\", \"cbs_class\": \"Link\", \"cbs_type\": \"Leaked Credential\", \"cbs_coa\": \"Member Side Action\", \"cbs_timestamp\": 1698049692779, \"cbs_updated_date\": \"2023-10-23T08:00:00+00:00\"}, \"xsoar_mirroring\": {\"mirror_direction\": \"Both\", \"mirror_id\": \"COMY123642991153\", \"mirror_instance\": \"CTM360_CyberBlindspot_instance_1\"}}",
"severity": 3,
"type": "Leaked Credential",
"xsoar_mirroring": {
"mirror_direction": "Both",
"mirror_id": "COMY123642991153",
"mirror_instance": "CTM360_CyberBlindspot_instance_1"
}
},
{
"CustomFields": {
"cbs_class": "Link",
"cbs_coa": "Member Side Action",
"cbs_status": "Member Feedback",
"cbs_subject": "2 customer credentials compromised (a86fda8)",
"cbs_timestamp": 1698051145410,
"cbs_type": "Leaked Credential",
"cbs_updated_date": "2023-10-23T08:00:00+00:00"
},
"externalstatus": "Member Feedback",
"name": "New leaked_credential with severity High found",
"occurred": "2023-10-23T08:00:00+00:00",
"rawJson": "{\"name\": \"New leaked_credential with severity High found\", \"occurred\": \"2023-10-23T08:00:00+00:00\", \"type\": \"Leaked Credential\", \"externalstatus\": \"Member Feedback\", \"severity\": 3, \"CustomFields\": {\"cbs_status\": \"Member Feedback\", \"cbs_subject\": \"2 customer credentials compromised (a86fda8)\", \"cbs_class\": \"Link\", \"cbs_type\": \"Leaked Credential\", \"cbs_coa\": \"Member Side Action\", \"cbs_timestamp\": 1698051145410, \"cbs_updated_date\": \"2023-10-23T08:00:00+00:00\"}, \"xsoar_mirroring\": {\"mirror_direction\": \"Both\", \"mirror_id\": \"COMY123073588255\", \"mirror_instance\": \"CTM360_CyberBlindspot_instance_1\"}}",
"severity": 3,
"type": "Leaked Credential",
"xsoar_mirroring": {
"mirror_direction": "Both",
"mirror_id": "COMY123073588255",
"mirror_instance": "CTM360_CyberBlindspot_instance_1"
}
}
]
}
}
Human Readable Output
Results
CustomFields externalstatus name occurred rawJson severity type xsoar_mirroring cbs_status: Member Feedback
cbs_subject: 2 customer credentials compromised (5d65815)
cbs_class: Link
cbs_type: Leaked Credential
cbs_coa: Member Side Action
cbs_timestamp: 1698049692779
cbs_updated_date: 2023-10-23T08:00:00+00:00Member Feedback New leaked_credential with severity High found 2023-10-23T08:00:00+00:00 {“name”: “New leaked_credential with severity High found”, “occurred”: “2023-10-23T08:00:00+00:00”, “type”: “Leaked Credential”, “externalstatus”: “Member Feedback”, “severity”: 3, “CustomFields”: {“cbs_status”: “Member Feedback”, “cbs_subject”: “2 customer credentials compromised (5d65815)”, “cbs_class”: “Link”, “cbs_type”: “Leaked Credential”, “cbs_coa”: “Member Side Action”, “cbs_timestamp”: 1698049692779, “cbs_updated_date”: “2023-10-23T08:00:00+00:00”}, “xsoar_mirroring”: {“mirror_direction”: “Both”, “mirror_id”: “COMY123642991153”, “mirror_instance”: “CTM360_CyberBlindspot_instance_1”}} 3 Leaked Credential mirror_direction: Both
mirror_id: COMY123642991153
mirror_instance: CTM360_CyberBlindspot_instance_1cbs_status: Member Feedback
cbs_subject: 2 customer credentials compromised (a86fda8)
cbs_class: Link
cbs_type: Leaked Credential
cbs_coa: Member Side Action
cbs_timestamp: 1698051145410
cbs_updated_date: 2023-10-23T08:00:00+00:00Member Feedback New leaked_credential with severity High found 2023-10-23T08:00:00+00:00 {“name”: “New leaked_credential with severity High found”, “occurred”: “2023-10-23T08:00:00+00:00”, “type”: “Leaked Credential”, “externalstatus”: “Member Feedback”, “severity”: 3, “CustomFields”: {“cbs_status”: “Member Feedback”, “cbs_subject”: “2 customer credentials compromised (a86fda8)”, “cbs_class”: “Link”, “cbs_type”: “Leaked Credential”, “cbs_coa”: “Member Side Action”, “cbs_timestamp”: 1698051145410, “cbs_updated_date”: “2023-10-23T08:00:00+00:00”}, “xsoar_mirroring”: {“mirror_direction”: “Both”, “mirror_id”: “COMY123073588255”, “mirror_instance”: “CTM360_CyberBlindspot_instance_1”}} 3 Leaked Credential mirror_direction: Both
mirror_id: COMY123073588255
mirror_instance: CTM360_CyberBlindspot_instance_1
ctm360-cbs-incident-close
Close a CBS incident.
Base Command
ctm360-cbs-incident-close
Input
| Argument Name | Description | Required |
|---|---|---|
| ticketId | “Ticket ID” of the incident to close. | Required |
Context Output
There is no context output for this command.
Command example
!ctm360-cbs-incident-close ticketId="COMX41148897294"
Human Readable Output
Incident closed successfully
ctm360-cbs-incident-request-takedown
Request a takedown of the asset where the incident was found.
Base Command
ctm360-cbs-incident-request-takedown
Input
| Argument Name | Description | Required |
|---|---|---|
| ticketId | “Ticket ID” of the incident to request takedown. | Required |
Context Output
There is no context output for this command.
Command example
!ctm360-cbs-incident-request-takedown ticketId="COMX415993788418"
Human Readable Output
Takedown request executed successfully
get-mapping-fields
Returns the list of fields for an incident type.
Base Command
get-mapping-fields
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
There is no context output for this command.
ctm360-cbs-incident-details
Fetch details of a single incident from the CyberBlindspot platform.
Base Command
ctm360-cbs-incident-details
Input
| Argument Name | Description | Required |
|---|---|---|
| ticketId | “Ticket ID” of the incident to close. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| CyberBlindspot.RemoteIncident.id | unknown | The unique ID for the incident record. |
| CyberBlindspot.RemoteIncident.brand | unknown | The organization the incident is associated with. |
| CyberBlindspot.RemoteIncident.coa | unknown | The course of action to take. |
| CyberBlindspot.RemoteIncident.class | unknown | The classification of the incident on remote server. |
| CyberBlindspot.RemoteIncident.status | unknown | The current status of the incident on remote server. |
| CyberBlindspot.RemoteIncident.severity | unknown | The severity of the incident. |
| CyberBlindspot.RemoteIncident.subject | unknown | The asset or title of the incident. |
| CyberBlindspot.RemoteIncident.type | unknown | The incident type on the remote server. |
| CyberBlindspot.RemoteIncident.remarks | unknown | The remarks about the incident. |
| CyberBlindspot.RemoteIncident.created_date | unknown | The creation date of the incident (legacy). |
| CyberBlindspot.RemoteIncident.updated_date | unknown | The date the incident last got updated (legacy). |
| CyberBlindspot.RemoteIncident.first_seen | unknown | The creation date of the incident. |
| CyberBlindspot.RemoteIncident.last_seen | unknown | The date the incident last got updated. |
| CyberBlindspot.RemoteIncident.timestamp | unknown | The timestamp of when the record was created. |
| CyberBlindspot.RemoteIncident.card_number | unknown | The compromised card’s number. |
| CyberBlindspot.RemoteIncident.cvv | unknown | The compromised card’s Card Verification Value (CVV). |
| CyberBlindspot.RemoteIncident.expiry_month | unknown | The compromised card’s expiration month. |
| CyberBlindspot.RemoteIncident.expiry_year | unknown | The compromised card’s expiration year. |
| CyberBlindspot.RemoteIncident.breach_source | unknown | The source of the breached data. |
| CyberBlindspot.RemoteIncident.domain | unknown | The domain related to the breached data or compromised device. |
| CyberBlindspot.RemoteIncident.email | unknown | The email found in the breached data. |
| CyberBlindspot.RemoteIncident.username | unknown | The username found in the breached data. |
| CyberBlindspot.RemoteIncident.password | unknown | The password found in the breached data or compromised account. |
| CyberBlindspot.RemoteIncident.executive_name | unknown | The executive member’s name related to the breached data. |
| CyberBlindspot.RemoteIncident.confirmation_time | unknown | The time of infringement confirmation. |
| CyberBlindspot.RemoteIncident.risks | unknown | The potential difficulties carried by the infringement. |
| CyberBlindspot.RemoteIncident.incident_status | unknown | The status of the infringement incident. |
| CyberBlindspot.RemoteIncident.screenshots | unknown | The screenshot evidence if available. |
| CyberBlindspot.RemoteIncident.date_compromised | unknown | The date of the compromise. |
| CyberBlindspot.RemoteIncident.computer_name | unknown | The name of the computer that was compromised. |
| CyberBlindspot.RemoteIncident.operating_system | unknown | The operating system of the compromised device. |
| CyberBlindspot.RemoteIncident.malware_path | unknown | The path in which the malware was installed on the compromised device. |
| CyberBlindspot.RemoteIncident.url_path | unknown | The path of the URL. |
| CyberBlindspot.RemoteIncident.masked_password | unknown | The masked password of the compromised account. |
| CyberBlindspot.RemoteIncident.software | unknown | The software that was compromised. |
| CyberBlindspot.RemoteIncident.user | unknown | The user of the compromised account. |
| CyberBlindspot.RemoteIncident.user_domain | unknown | The domain of the compromised user. |
| CyberBlindspot.RemoteIncident.website | unknown | The website that was compromised. |
| CyberBlindspot.RemoteIncident.sources | unknown | The sources reporting the compromise. |
| CyberBlindspot.RemoteIncident.source_uri | unknown | The source URI of the compromise report. |
| CyberBlindspot.RemoteIncident.hostname | unknown | The hostname of the compromised device. |
| CyberBlindspot.RemoteIncident.stealer_family | unknown | The family of the malware. |
| CyberBlindspot.RemoteIncident.external_link | unknown | The external link to the remote platform. |
| CyberBlindspot.RemoteIncident.compromise_details | unknown | The details of the compromise. |
| CyberBlindspot.RemoteIncident.platform | unknown | The social network platform for social media fraud findings. |
| CyberBlindspot.RemoteIncident.risk_score | unknown | The numeric risk score from CBS. |
| CyberBlindspot.RemoteIncident.money_mule_id | unknown | The CBS money mule finding ID. |
| CyberBlindspot.RemoteIncident.account_identifier | unknown | The account identifier tied to the money mule. |
| CyberBlindspot.RemoteIncident.suspect_names | unknown | The names associated with the money mule. |
| CyberBlindspot.RemoteIncident.suspect_emails | unknown | The email addresses associated with the money mule. |
| CyberBlindspot.RemoteIncident.suspect_phones | unknown | The phone numbers associated with the money mule. |
| CyberBlindspot.RemoteIncident.transfer_amount | unknown | The transfer amount when present. |
| CyberBlindspot.RemoteIncident.transfer_currency | unknown | The currency code for the transfer. |
| CyberBlindspot.RemoteIncident.bank_account_holder_name | unknown | The name on the bank account. |
| CyberBlindspot.RemoteIncident.bank_name | unknown | The bank name tied to the money mule. |
| CyberBlindspot.RemoteIncident.bank_account_country | unknown | The country of the bank account. |
| CyberBlindspot.RemoteIncident.bic | unknown | The Bank Identifier Code. |
| CyberBlindspot.RemoteIncident.finding_id | unknown | The CBS gambling-site finding ID. |
| CyberBlindspot.RemoteIncident.url | unknown | The primary gambling site URL. |
| CyberBlindspot.RemoteIncident.submitted_url | unknown | The URL submitted to CBS for scanning. |
| CyberBlindspot.RemoteIncident.landing_url | unknown | The landing page URL observed for the site. |
| CyberBlindspot.RemoteIncident.title | unknown | The page title observed during scan. |
| CyberBlindspot.RemoteIncident.resolving_ip | unknown | The resolved IP for the gambling site. |
| CyberBlindspot.RemoteIncident.tags | unknown | The tags applied to the gambling site finding. |
| CyberBlindspot.RemoteIncident.status_code | unknown | The HTTP status code from scan. |
| CyberBlindspot.RemoteIncident.url_status | unknown | The URL reachability status. |
| CyberBlindspot.RemoteIncident.scan_status | unknown | The scan completion status. |
| CyberBlindspot.RemoteIncident.enrichment | unknown | The DNS enrichment payload. |
| CyberBlindspot.RemoteIncident.external_links | unknown | The external links discovered on the gambling site. |
| CyberBlindspot.RemoteIncident.internal_links | unknown | The internal links discovered on the gambling site. |
ctm360-cbs-incident-retrieve-screenshots
Retrieves screenshot evidence if available.
Base Command
ctm360-cbs-incident-retrieve-screenshots
Input
| Argument Name | Description | Required |
|---|---|---|
| files | The files to retrieve. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.Name | string | FileName. |
| InfoFile.EntryID | string | The EntryID of the report. |
| InfoFile.Size | number | File Size. |
| InfoFile.Type | string | File type e.g. “PE”. |
| InfoFile.Info | string | Basic information of the file. |
get-remote-data
Gets remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes.
Base Command
get-remote-data
Input
| Argument Name | Description | Required |
|---|---|---|
| id | The incident ID. | Required |
| lastUpdate | Retrieves entries that were created after lastUpdate. | Required |
Context Output
There is no context output for this command.
get-modified-remote-data
Gets the list of incidents that were modified since the last update time. Note that this method is here for debugging purposes. The get-modified-remote-data command is used as part of a Mirroring feature, which is available in Cortex XSOAR from version 6.1.
Base Command
get-modified-remote-data
Input
| Argument Name | Description | Required |
|---|---|---|
| lastUpdate | A date string in local time representing the last time the incident was updated. The incident is only returned if it was modified after the last update time. | Required |
Context Output
There is no context output for this command.
update-remote-system
Updates the remote system with local changes.
Base Command
update-remote-system
Input
| Argument Name | Description | Required |
|---|---|---|
| remoteId | Remote ID of incident to update in the remote system. | Required |
Context Output
There is no context output for this command.
Incident Mirroring
You can enable incident mirroring between Cortex XSOAR incidents and CTM360 CyberBlindspot corresponding events (available from Cortex XSOAR version 6.0.0).
To set up the mirroring:
- Enable Fetching incidents in your instance configuration.
-
In the Mirroring Direction integration parameter, select in which direction the incidents should be mirrored:
Option Description None Turns off incident mirroring. Incoming Any changes in CTM360 CyberBlindspot events (mirroring incoming fields) will be reflected in Cortex XSOAR incidents. Outgoing Any changes in Cortex XSOAR incidents will be reflected in CTM360 CyberBlindspot events (outgoing mirrored fields). Incoming And Outgoing Changes in Cortex XSOAR incidents and CTM360 CyberBlindspot events will be reflected in both directions.
Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents.
Important Note: To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and CTM360 CyberBlindspot.
Configuration parameters
mirror_direction— Incident Mirroring Directionmodule_to_use— Module To Useretrieve_screenshots— Retrieve Screenshotsfirst_fetch— First fetch (<number> <time unit>, e.g., 12 hours. Default is `7 days`)api_key— (required)max_fetch— Maximum Number of Incidents per Fetch (required)isFetch— Fetch incidentsinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsincidentType— Incident typeincidentFetchInterval— Incidents Fetch Interval
Commands (9)
-
ctm360-cbs-incident-closeClose a CBS incident.
-
ctm360-cbs-incident-detailsFetch details of a single incident from the CyberBlindspot platform.
-
ctm360-cbs-incident-listGet the list of incidents from CBS.
-
ctm360-cbs-incident-request-takedownRequest a takedown of the asset where the incident was found.
-
ctm360-cbs-incident-retrieve-screenshotsRetrieves screenshot evidence if available.
-
get-mapping-fieldsReturns the list of fields for an incident type.
-
get-modified-remote-dataGets the list of incidents that were modified since the last update time. Note that this method is here for debugging purposes. The get-modified-remote-data command is used as part of a Mirroring feature, which is available in Cortex XSOAR from version 6.1.
-
get-remote-dataGets remote data from a remote incident. This method does not update the current incident, and should be used for debugging purposes.
-
update-remote-systemUpdates the remote system with local changes.
from typing import Any import demistomock as demisto # noqa: F401 import urllib3 from CommonServerPython import * # noqa: F401 from CommonServerUserPython import * # noqa """ IMPORTS """ from inspect import getfullargspec # Disable insecure warnings urllib3.disable_warnings() """ CONSTANTS """ INFO = "info" ERROR = "error" DEBUG = "debug" MAX_RETRIES = 5 MIRROR_LIMIT = 1000 ABSOLUTE_MAX_FETCH = 200 MAX_FETCH = arg_to_number(demisto.params().get("max_fetch")) or 25 MAX_FETCH = min(MAX_FETCH, ABSOLUTE_MAX_FETCH) RETRIEVE_SCREENSHOTS = bool(demisto.params().get("retrieve_screenshots", True)) DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" # ISO8601 format with UTC, default in XSOAR CBS_OUTGOING_DATE_FORMAT = "%d-%m-%Y %H:%M" CBS_INCOMING_DATE_FORMAT = "%d-%m-%Y %I:%M:%S %p" CBS_BASE_URL = "https://cbs.ctm360.com" CBS_API_ENDPOINT = "/api/v2" API = { "FETCH": "/incidents/xsoar", "GET_SCREENSHOT": "/incidents/x_platform/screenshots", "CLOSE_INCIDENT": "/incidents/close_incident/", "REQUEST_TAKEDOWN": "/incidents/request_takedown/", } LOGGING_PREFIX = "[CYBER-BLINDSPOT]" CBS_MODULE_DISPLAY_TO_TYPE = { "Incidents": "incidents", "Compromised Cards": "compromised_cards", "Breached Credentials": "breached_credentials", "Malware Logs": "malware_logs", "Domain Infringement": "domain_infringement", "Subdomain Infringement": "subdomain_infringement", "Social Media Fraud": "social_media_fraud", "Gambling Sites": "gambling_sites", "Money Mules": "money_mules", } CBS_DEFAULT_MODULE_DISPLAY = "Incidents" CBS_DEFAULT_MODULE_TYPE = "incidents" DEFAULT_FIELDS = [ {"name": "first_seen", "description": "The creation date of the incident."}, {"name": "last_seen", "description": "The date the incident was last updated."}, {"name": "timestamp", "description": "The timestamp of when the record was created."}, {"name": "brand", "description": "The organization the incident belongs to."}, {"name": "status", "description": "The current state of affairs of the incident."}, {"name": "severity", "description": "The severity of the incident."}, {"name": "remarks", "description": "The remarks about the incident."}, {"name": "type", "description": "The incident type."}, {"name": "id", "description": "The unique ID for the incident record."}, {"name": "external_link", "description": "The external link to the remote platform."}, ] CBS_INCIDENT_FIELDS = [ {"name": "subject", "description": "The asset or title of the incident."}, {"name": "screenshots", "description": "The screenshot evidence if available."}, {"name": "class", "description": "The subject class."}, {"name": "coa", "description": "The possible course of action."}, *DEFAULT_FIELDS, ] CBS_CARD_FIELDS = [ {"name": "card_number", "description": "The compromised card's number."}, {"name": "cvv", "description": "The compromised card's Card Verification Value (CVV)."}, {"name": "expiry_month", "description": "The compromised card's expiration month."}, {"name": "expiry_year", "description": "The compromised card's expiration year."}, *DEFAULT_FIELDS, ] CBS_MALWARE_LOG_FIELDS = [ {"name": "masked_password", "description": "The masked password related to the breached data."}, {"name": "password", "description": "The password found in the breached data or compromised account."}, {"name": "software", "description": "The software related to the breached data."}, {"name": "user", "description": "The user related to the breached data."}, {"name": "user_domain", "description": "The domain of the user related to the breached data."}, {"name": "website", "description": "The website related to the breached data."}, {"name": "sources", "description": "The sources related to the breached data."}, {"name": "source_uri", "description": "The source URI related to the breached data."}, {"name": "domain", "description": "The domain related to the breached data or compromised device."}, {"name": "hostname", "description": "The hostname related to the breached data."}, {"name": "stealer_family", "description": "The family of the malware."}, {"name": "compromise_details", "description": "The details of the compromise."}, {"name": "date_compromised", "description": "The date the malware was compromised."}, {"name": "computer_name", "description": "The name of the computer that was compromised."}, {"name": "operating_system", "description": "The operating system of the computer that was compromised."}, {"name": "malware_path", "description": "The path of the malware."}, {"name": "url_path", "description": "The URL path of the malware."}, *DEFAULT_FIELDS, ] CBS_CRED_FIELDS = [ {"name": "breach_source", "description": "The source of breached data."}, {"name": "domain", "description": "The domain related to the breached data."}, {"name": "email", "description": "The email found in the breached data."}, {"name": "username", "description": "The username found in the breached data."}, {"name": "executive_name", "description": "The executive member's name related to the breached data."}, {"name": "password", "description": "The password found in the breached data."}, *DEFAULT_FIELDS, ] CBS_DOMAIN_INFRINGE_FIELDS = [ {"name": "confirmation_time", "description": "The time of infringement confirmation."}, {"name": "risks", "description": "The potential difficulties carried by the infringement."}, {"name": "incident_status", "description": "The status of the infringement incident."}, *DEFAULT_FIELDS, ] CBS_SMF_FIELDS = [ {"name": "platform", "description": "The social network platform for social media fraud findings."}, {"name": "subject", "description": "The subject URL or profile link."}, {"name": "risk_score", "description": "The numeric risk score from CBS."}, {"name": "risks", "description": "The risk indicators associated with the finding."}, {"name": "incident_status", "description": "The platform-specific incident status."}, *DEFAULT_FIELDS, ] CBS_MM_FIELDS = [ {"name": "money_mule_id", "description": "The CBS money mule finding ID."}, {"name": "account_identifier", "description": "The account identifier tied to the money mule."}, {"name": "suspect_names", "description": "The names associated with the money mule."}, {"name": "suspect_emails", "description": "The email addresses associated with the money mule."}, {"name": "suspect_phones", "description": "The phone numbers associated with the money mule."}, {"name": "transfer_amount", "description": "The transfer amount when present."}, {"name": "transfer_currency", "description": "The currency code for the transfer."}, {"name": "bank_account_holder_name", "description": "The name on the bank account."}, {"name": "bank_name", "description": "The bank name tied to the money mule."}, {"name": "bank_account_country", "description": "The country of the bank account."}, {"name": "bic", "description": "The Bank Identifier Code."}, *DEFAULT_FIELDS, ] CBS_GS_FIELDS = [ {"name": "finding_id", "description": "The CBS gambling-site finding ID."}, {"name": "url", "description": "The primary gambling site URL."}, {"name": "submitted_url", "description": "The URL submitted to CBS for scanning."}, {"name": "landing_url", "description": "The landing page URL observed for the site."}, {"name": "title", "description": "The page title observed during scan."}, {"name": "resolving_ip", "description": "The resolved IP for the gambling site."}, {"name": "tags", "description": "The tags applied to the gambling site finding."}, {"name": "status_code", "description": "The HTTP status code from scan."}, {"name": "url_status", "description": "The URL reachability status."}, {"name": "scan_status", "description": "The scan completion status."}, {"name": "enrichment", "description": "The DNS enrichment payload."}, {"name": "external_links", "description": "The external links discovered on the gambling site."}, {"name": "internal_links", "description": "The internal links discovered on the gambling site."}, *DEFAULT_FIELDS, ] MIRROR_DIRECTION = {"None": None, "Incoming": "In", "Outgoing": "Out", "Incoming And Outgoing": "Both"}.get( demisto.params().get("mirror_direction", "None"), None ) def resolve_cbs_module(module_to_use: str | None = None) -> str: """Resolve API module_type from instance config. Pre-upgrade instances may omit module_to_use; treat missing/blank/unknown as Incidents. """ if module_to_use is None: module_to_use = demisto.params().get("module_to_use", CBS_DEFAULT_MODULE_DISPLAY) if not module_to_use or not str(module_to_use).strip(): return CBS_DEFAULT_MODULE_TYPE return CBS_MODULE_DISPLAY_TO_TYPE.get(module_to_use, CBS_DEFAULT_MODULE_TYPE) class Instance: def __init__(self, **kwargs) -> None: self.module: str = kwargs.get("module", "incidents") self.list_prefix = "CyberBlindspot.IncidentList" self.details_prefix = "CyberBlindspot.RemoteIncident" match self.module: case "compromised_cards": self.mapping_fields = CBS_CARD_FIELDS case "breached_credentials": self.mapping_fields = CBS_CRED_FIELDS case "malware_logs": self.mapping_fields = CBS_MALWARE_LOG_FIELDS case "domain_infringement": self.mapping_fields = CBS_DOMAIN_INFRINGE_FIELDS case "subdomain_infringement": self.mapping_fields = CBS_DOMAIN_INFRINGE_FIELDS case "social_media_fraud": self.mapping_fields = CBS_SMF_FIELDS case "money_mules": self.mapping_fields = CBS_MM_FIELDS case "gambling_sites": self.mapping_fields = CBS_GS_FIELDS case _: self.mapping_fields = CBS_INCIDENT_FIELDS INSTANCE = Instance(module=resolve_cbs_module()) INTEGRATION_INSTANCE = demisto.integrationInstance() """ CLIENT CLASS """ class Client(BaseClient): """Client class to interact with the service API This Client implements API calls, and does not contain any XSOAR logic. Should only do requests and return data. It inherits from BaseClient defined in CommonServer Python. Most calls use _http_request() that handles proxy, SSL verification, etc. For this implementation, no special attributes defined """ def test_configuration(self, params: dict[str, Any]) -> list[dict[str, Any]]: """Send request to test :param params: Parameters to be sent in the request :type params: dict[str, Any] :return: List containing the incidents :rtype: List[dict[str, Any]] """ response = self._http_request(method="GET", url_suffix=CBS_API_ENDPOINT + API.get("FETCH", ""), params=params) log(DEBUG, "at client's test function") if response.get("statusCode") != 200: raise DemistoException(f'Error received: {response.get("errors", "request was not successful")}') return response.get("hits", []) def get_screenshot_files(self, params: dict[str, Any]) -> list[dict[str, Any]]: """Send request to get screenshot(s) :param params: Parameters to be sent in the request :type params: dict[str, Any] :return: List of dictionaries containing file information :rtype: list[dict[str, Any]] """ log(DEBUG, "at client's get_screenshot_files function") log(DEBUG, f"{params=}") response = self._http_request( method="POST", retries=MAX_RETRIES, backoff_factor=10, status_list_to_retry=[400, 429, 500], url_suffix=CBS_API_ENDPOINT + API.get("GET_SCREENSHOT", ""), json_data=params, params={"t": datetime.now().timestamp()}, ) log(DEBUG, f"{response=}") return response.get("results", []) or [] # Return empty list if results is None def fetch_incidents(self, params: dict[str, Any]) -> list[dict[str, Any]]: """Send request to fetch list of incidents :param params: Parameters to be sent in the request :type params: dict[str, Any] :return: List containing the incidents :rtype: List[dict[str, Any]] """ response = self._http_request( method="GET", retries=MAX_RETRIES, backoff_factor=10, status_list_to_retry=[400, 429, 500], url_suffix=CBS_API_ENDPOINT + API.get("FETCH", ""), params=params, ) log(DEBUG, "at client's fetch function") if response.get("statusCode") != 200: raise DemistoException(f'Error received: {response.get("message")}') incident_list = response.get("hits", []) return incident_list def fetch_incident(self, params: dict[str, Any]) -> dict[str, Any]: """Send a request to fetch a certain incident :param params: Parameters to be sent in the request :type params: dict[str, Any] :return: Dictionary containing the incident data :rtype: dict[str, Any] """ response = self._http_request( method="GET", retries=MAX_RETRIES, backoff_factor=10, status_list_to_retry=[400, 429, 500], url_suffix=CBS_API_ENDPOINT + API.get("FETCH", ""), params=params, ) log(DEBUG, "at client's fetch function") incident_list = response.get("hits", []) if not incident_list: return {} return incident_list[0] def close_incident(self, args: dict[str, Any]) -> dict[str, Any]: """Send incident close request for a certain incident :param args: Arguments to be sent in the request :type args: dict[str, Any] :return: Response from the remote server carrying the result of the request :rtype: dict[str, Any] """ response = self._http_request( method="POST", retries=MAX_RETRIES, backoff_factor=10, status_list_to_retry=[400, 429, 500], url_suffix=CBS_API_ENDPOINT + API["CLOSE_INCIDENT"], data=args, params={"t": datetime.now().timestamp()}, ) log(DEBUG, f'Closing returned status {response.get("statusCode")}') if response.get("statusCode") != 200: log(ERROR, f'Incident could not be closed: Error Code {response.get("statusCode")}') else: log(INFO, response.get("message", "")) return response def request_takedown(self, args: dict[str, Any]) -> dict[str, Any]: """Send incident takedown request for a certain incident :param args: Arguments to be sent in the request :type args: dict[str, Any] :return: Response from the remote server carrying the result of the request :rtype: dict[str, Any] """ response = self._http_request( method="POST", retries=MAX_RETRIES, backoff_factor=10, status_list_to_retry=[400, 429, 500], url_suffix=CBS_API_ENDPOINT + API["REQUEST_TAKEDOWN"], data=args, params={"t": datetime.now().timestamp()}, ) log(DEBUG, f'Takedown request returned status {response.get("statusCode")}') if response.get("statusCode") != 200: log(ERROR, f'Takedown Request Failed: Error Code {response.get("statusCode")}') else: log(INFO, response.get("message", "")) return response """ HELPER FUNCTIONS """ def log(level: str, msg: str): { "info": demisto.info, "error": demisto.error, "debug": demisto.debug, }[level.lower()](f"{LOGGING_PREFIX} {msg}") def convert_to_demisto_severity(severity: str) -> int | float: """Converts the CyberBlindspot incident severity level ('Unknown', 'Info', 'Low', 'Medium', 'High', 'Critical') to XSOAR incident severity (0 to 4). :param severity: severity as returned from the CyberBlindspot API :type severity: ``str`` :return: _description_ :rtype: ``int | float`` """ return { "informational": IncidentSeverity.INFO, "info": IncidentSeverity.INFO, "low": IncidentSeverity.LOW, "medium": IncidentSeverity.MEDIUM, "high": IncidentSeverity.HIGH, "critical": IncidentSeverity.CRITICAL, "fyi": IncidentSeverity.UNKNOWN, }[severity.lower()] def convert_time_string( time_string: str, input_format_string: str, output_format: str = "", timestamp: bool = False, is_utc=False, in_iso_format=False, **parser_args, ) -> datetime | str | int: """helper function to convert a time string into another format or get the timestamp from it :param time_string: Input time string :type time_string: ``str`` :param input_format_string: Format string of the input_time_string :type input_format_string: ``str`` :param output_format: The format string to convert a time string into, defaults to '' :type output_format: str, optional :param timestamp: A flag to signal whether or not to return a timestamp, defaults to False :type timestamp: bool, optional :return: A datetime object, a time string or timestamp integer :rtype: ``datetime|str|int`` """ try: output = dateparser.parse(time_string, [input_format_string], **parser_args) if not isinstance(output, datetime): raise ValueError("The passed date string and/or format string is not valid") if is_utc: output = output.replace(tzinfo=timezone.utc) if in_iso_format: return output.isoformat() if output_format: return output.strftime(output_format) elif timestamp: return int(output.timestamp() * 1000) else: return output except ValueError as err: log(ERROR, f"An error was encountered at `convert_time_string()` {err=}") return "" def normalize_timestamp(value: Any) -> Any: """Normalize CBS record timestamp to epoch milliseconds. CBS modules may return timestamp as millis (int), numeric string, or ISO datetime string. Fetch cursor logic requires a consistent numeric value for sorting and date_from. """ if value is None or value == "": return value if isinstance(value, bool): return value if isinstance(value, int | float): return int(value) if isinstance(value, str): stripped = value.strip() if stripped.isdigit(): return int(stripped) parsed = convert_time_string(stripped, "", timestamp=True, is_utc=True) if isinstance(parsed, int): return parsed return value def deduplicate_and_create_incidents(fetched_incidents: List, last_run_incident_identifiers: List[str]) -> tuple[list, list]: """De-duplicates the fetched incidents and creates a list of actionable incidents. :param fetched_incidents: Context of the events fetched. :type fetched_incidents: List :param last_run_incident_identifiers: List of ids from the events fetched in the last run. :type last_run_incident_identifiers: List[str] :return: Returns updated list of event ids and unique incidents that should be created. :rtype: ``tuple[list,list]`` """ log(DEBUG, "at Dedup function") incidents: List[dict[str, Any]] = [] new_incident_ids = [] for incident in fetched_incidents: try: incident_id = incident.get("id") new_incident_ids.append(incident_id) except Exception as e: log(ERROR, f"Skipping insertion of current incident. Error while fetching ID from {incident=}. Error: {str(e)}") continue if last_run_incident_identifiers and incident_id in last_run_incident_identifiers: log(INFO, f"Skipping insertion of current incident since it already exists. \n\n {incident=}") continue else: log(DEBUG, "Creating unique incident") unique_mapped_incident = map_and_create_incident(incident) incidents.append(unique_mapped_incident) log(DEBUG, f"{incidents[:1]=}") return new_incident_ids, incidents def map_and_create_incident(unmapped_incident: dict) -> dict: """Use dictionary of unmapped fetched incident to create a mapped dictionary :param unmapped_incident: Fetched incident (unmapped) :type unmapped_incident: ``dict`` :return: Incident in format ready for XSOAR :rtype: ``dict`` """ unmapped_incident.pop("screenshots", "") incident_id: str = unmapped_incident.pop("id", "") mapped_severity = convert_to_demisto_severity(unmapped_incident.pop("severity", "low")) mapped_incident = { "name": unmapped_incident.pop("remarks", ""), "occurred": convert_time_string( unmapped_incident.pop("first_seen", ""), CBS_INCOMING_DATE_FORMAT, in_iso_format=True, is_utc=True ), "externalstatus": unmapped_incident.pop("status", "monitoring"), "externallink": unmapped_incident.pop("external_link", ""), "severity": mapped_severity, "CustomFields": { "cbs_type": unmapped_incident.pop("type", ""), "cbs_module": INSTANCE.module, "cbs_coa": unmapped_incident.pop("coa", ""), "cbs_updated_date": convert_time_string( unmapped_incident.pop("last_seen", ""), CBS_INCOMING_DATE_FORMAT, in_iso_format=True, is_utc=True ), **unmapped_incident, }, } if "timestamp" in mapped_incident["CustomFields"]: mapped_incident["CustomFields"]["timestamp"] = normalize_timestamp(mapped_incident["CustomFields"]["timestamp"]) if MIRROR_DIRECTION: mapped_incident["xsoar_mirroring"] = { "mirror_direction": MIRROR_DIRECTION, "mirror_id": incident_id, "mirror_instance": INTEGRATION_INSTANCE, } mapped_incident["rawJson"] = json.dumps(mapped_incident) return mapped_incident def to_snake_case(input_string: str) -> str: """helper function to return passed strings in snake_case :param input_string: Input string to change into snake case :type input_string: ``str`` :return: A string in snake case :rtype: ``str`` """ return "".join(["_" + i.lower() if i.isupper() else i for i in input_string]).lstrip("_") """ COMMAND FUNCTIONS """ def test_module(client: Client, params) -> str: """Tests API connectivity and authentication' Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful. Raises exceptions if something goes wrong. :type client: ``Client`` :param Client: client to use :return: `'ok'` if test passed, anything else will fail the test. :rtype: ``str`` """ message: str = "" args: dict[str, Any] = {} try: mirror_direction = params.get("mirror_direction", "") first_fetch = params.get("first_fetch", "") max_fetch = arg_to_number(params.get("max_fetch", "")) date_from = params.get("date_from", "") date_to = params.get("date_to", "") api_key = params.get("api_key", {}).get("password", "") if mirror_direction not in ["None", "Incoming", "Outgoing", "Incoming And Outgoing"]: log(INFO, 'Invalid "Mirror Direction" Value') raise DemistoException('Invalid "Mirroring Direction" Value') if first_fetch and not dateparser.parse(first_fetch): log(INFO, 'Invalid "First Fetch" Value') raise DemistoException('Invalid "First Fetch" Value') if max_fetch and (max_fetch <= 0 or max_fetch > ABSOLUTE_MAX_FETCH): log(INFO, f'Invalid "Max Fetch" Value. Should be between 1 to {ABSOLUTE_MAX_FETCH}') raise DemistoException(f'Invalid "Max Fetch" Value. Should be between 1 to {ABSOLUTE_MAX_FETCH}') else: args["max_hits"] = max_fetch if date_from and not dateparser.parse(date_from, [CBS_OUTGOING_DATE_FORMAT]): log(INFO, 'Invalid "Date From" Value (Does not match format "%d-%m-%Y %H:%M")') raise DemistoException('Invalid "Date From" Value (Does not match format "%d-%m-%Y %H:%M")') elif date_from: args["date_from"] = convert_time_string(date_from, CBS_OUTGOING_DATE_FORMAT, timestamp=True) if date_to and not dateparser.parse(date_to, [CBS_OUTGOING_DATE_FORMAT]): log(INFO, 'Invalid "Date To" Value (Does not match format "%d-%m-%Y %H:%M")') raise DemistoException('Invalid "Date To" Value (Does not match format "%d-%m-%Y %H:%M")') elif date_to: args["date_to"] = convert_time_string(date_to, CBS_OUTGOING_DATE_FORMAT, timestamp=True) if not api_key: log(INFO, 'Invalid "API Key" Value') raise DemistoException('Invalid "API Key" Value') args["module_type"] = resolve_cbs_module(params.get("module_to_use")) incidents = client.test_configuration(args) if max_fetch and len(incidents) > max_fetch: log(INFO, f"Incidents fetched exceed the limit, removing the excess {len(incidents) - max_fetch} incidents.") incidents = incidents[:: max_fetch - 1] message = "ok" except DemistoException as e: expected_words = [ "Forbidden", "Authorization", "Mirroring Direction", "First Fetch", "Max Fetch", "Date From", "Date To", "API Key", "Module", "does not match format '%d-%m-%Y %H:%M'", ] for word in expected_words: log(DEBUG, "Exception in test_module()") if word in str(e.message): message = e.message else: raise e return message def fetch_incidents( client: Client, last_fetch_ids: list[str], params: dict[str, Any], last_run: dict[str, Any] ) -> tuple[dict, list[dict]]: """Helper function to call client's `fetch_incidents` function. Args: client (Client): client last_fetch_ids (list[str]): The list of unique incident IDs that were fetched in the last run params (dict[str, Any]): GET params to send with the fetch request Returns: tuple[dict, list[dict]]: The next run object and the list of incidents minus incidents from last run """ incidents = client.fetch_incidents(params) max_fetch = arg_to_number(params.get("max_hits")) or MAX_FETCH if max_fetch and len(incidents) > max_fetch: log(INFO, f"Incidents fetched exceed the limit, removing the excess {len(incidents) - max_fetch} incidents.") incidents = incidents[:: max_fetch - 1] if not incidents and not last_run: log(INFO, "No incidents returned, and no last run data was found") return {}, [] incident_ids, unique_incidents = deduplicate_and_create_incidents(incidents, last_fetch_ids) log(INFO, f"Received {len(incidents) - len(unique_incidents)} duplicates incidents to skip.") log(INFO, f"Calculated {len(incident_ids)} id(s).") dates = sorted( normalized for incident in unique_incidents if (normalized := normalize_timestamp(incident["CustomFields"].get("timestamp"))) not in (None, "") ) last_fetched_timestamp = dates[-1] if dates else last_run.get("last_fetched_timestamp") log(INFO, f"setting last fetched timestamp - {last_fetched_timestamp=}") next_run = {"last_fetched_timestamp": last_fetched_timestamp, "last_fetch_ids": incident_ids} return next_run, unique_incidents def build_fetch_params(demisto_params: dict[str, Any], last_run: dict[str, Any]) -> dict[str, Any]: """Build API params for fetch-incidents.""" last_fetched_timestamp = last_run.get("last_fetched_timestamp", "") if last_fetched_timestamp not in ("", None): last_fetched_timestamp = normalize_timestamp(last_fetched_timestamp) first_fetch = demisto_params.get("first_fetch", "7 days") try: dateparser.parse(f"{first_fetch} UTC") except Exception: log(DEBUG, "first_fetch is not parsable, setting to `7 days`") first_fetch = "7 days" if not last_fetched_timestamp: log(DEBUG, f"Fetch is set to fetch from the {first_fetch} ago.") params: dict[str, Any] = { "date_field": "@timestamp", "order": "asc", "max_hits": MAX_FETCH, "module_type": INSTANCE.module, "date_from": last_fetched_timestamp if last_fetched_timestamp else convert_time_string(f"{first_fetch} UTC", "", timestamp=True), "t": datetime.now().timestamp() * 1000, } if "domain_infringement" in INSTANCE.module: params["finding_status"] = demisto_params.get("finding_status", "") params["risk_score_min"] = demisto_params.get("risk_score_min", "") params["risk_score_max"] = demisto_params.get("risk_score_max", "") return params def get_remote_data_command(client: Client, args: dict): """get-remote-data command: Returns an updated incident and error entry (if needed) Args: client: client args (dict): The command arguments close_incident (bool): Indicates whether to close the corresponding XSOAR incident if the incident has been closed on CBS's end. close_end_statuses (bool): Specifies whether "End Status" statuses on CBS should be closed when mirroring. Returns: GetRemoteDataResponse: The Response containing the updated incident to mirror and its entries """ entries = [] updated_incident = {} remote_args = GetRemoteDataArgs(args) remote_incident_id = remote_args.remote_incident_id last_update = parse_date_string(remote_args.last_update) last_update_ts = last_update.timestamp() * 1000 if isinstance(last_update, datetime) else -1 log(DEBUG, f"Performing get-remote-data command for the incident: {remote_incident_id}") params = {"ticket_id": remote_incident_id, "module_type": INSTANCE.module, "t": datetime.now().timestamp() * 1000} updated_incident = client.fetch_incident(params) incident_updated_date = updated_incident.get("last_seen", "") settings = {"TIMEZONE": "UTC"} remote_incident_last_update_ts = convert_time_string( incident_updated_date, CBS_INCOMING_DATE_FORMAT, timestamp=True, settings=settings ) log(DEBUG, f"{updated_incident=}") log(DEBUG, f"{last_update_ts=}") log(DEBUG, f"{remote_incident_last_update_ts=}") status = updated_incident.get("status", "").lower() if status == "closed": log(DEBUG, f"Incident seems to be {status}. Adding closing entry") log(INFO, f"Updating incident {remote_incident_id} with status: {status}") close_reason = f"Incident was {status} on CyberBlindspot." entries.append( { "Type": EntryType.NOTE, "Contents": {"dbotIncidentClose": True, "closeReason": close_reason}, "ContentsFormat": EntryFormat.JSON, } ) elif status in ["disregarded", "unconfirmed"]: log(DEBUG, f"Incident seems to be {status}. Adding entry") log(INFO, f"Updating incident {remote_incident_id} with status: {status}") note = f"Incident was {status} on CyberBlindspot. Not closed yet." entries.append({"Type": EntryType.NOTE, "Contents": note, "ContentsFormat": EntryFormat.TEXT}) elif status == "auto_resolved": log(DEBUG, "Incident seems to be resolved. Adding resolution entry") log(INFO, f"Updating incident {remote_incident_id} with status: {status}") note = "Incident was resolved through monitoring on CyberBlindspot. Not closed yet." entries.append({"Type": EntryType.NOTE, "Contents": note, "ContentsFormat": EntryFormat.TEXT}) elif status == "resolved": log(DEBUG, "Incident seems to be resolved. Adding resolution entry") log(INFO, f"Updating incident {remote_incident_id} with status: {status}") note = "Incident was resolved by response team on CyberBlindspot. Not closed yet." entries.append({"Type": EntryType.NOTE, "Contents": note, "ContentsFormat": EntryFormat.TEXT}) elif status == "wip": log(DEBUG, "Incident seems to be undergoing a process. Adding processing entry") log(INFO, f"Updating incident {remote_incident_id} with status: {status}") note = "Incident is undergoing a process on CyberBlindspot. Actions will be unavailable until processing is done." entries.append({"Type": EntryType.NOTE, "Contents": note, "ContentsFormat": EntryFormat.TEXT}) elif status == "monitoring": log(DEBUG, "Incident seems to be undergoing monitoring. Adding monitoring entry") log(INFO, f"Updating incident {remote_incident_id} with status: {status}") note = "Incident is being monitored on CyberBlindspot. Actions will be unavailable until monitoring is done." entries.append({"Type": EntryType.NOTE, "Contents": note, "ContentsFormat": EntryFormat.TEXT}) else: log(DEBUG, f'This status value `{updated_incident.get("status")}` for incident {remote_incident_id}. Had some issue') return GetRemoteDataResponse([], []) log(DEBUG, f"Updated incident {remote_incident_id}") mapped_updated_incident = map_and_create_incident(updated_incident) return GetRemoteDataResponse(mirrored_object=mapped_updated_incident, entries=entries) def get_modified_remote_data_command(client: Client, args): """Gets the list of all incidents that have changed since a given timestamp Args: client: http Client args (dict): The command arguments Returns: GetModifiedRemoteDataResponse: The response containing the list of ids of incidents changed """ modified_incident_ids: list = [] remote_args = GetModifiedRemoteDataArgs(args) last_timestamp = convert_time_string(remote_args.last_update, "", timestamp=True) log(DEBUG, f"Performing get-modified-remote-data command with : {last_timestamp}({remote_args.last_update})") params = { "date_field": "last_seen", "order": "asc", "date_from": last_timestamp, "max_hits": ABSOLUTE_MAX_FETCH, "module_type": INSTANCE.module, "t": datetime.now().timestamp() * 1000, } modified_incident_ids.extend(item["id"] for item in client.fetch_incidents(params)) if len(modified_incident_ids) >= MIRROR_LIMIT: log(INFO, f"Warning: More than {MIRROR_LIMIT} incidents have been modified since the last update.") return GetModifiedRemoteDataResponse(modified_incident_ids=modified_incident_ids) def update_remote_system_command(client: Client, args: dict): parsed_args = UpdateRemoteSystemArgs(args) log(DEBUG, f"Got the following update args:\n\n{parsed_args.entries=},") log(DEBUG, f"\n\n{parsed_args.data=}, ") log(DEBUG, f"\n\n{parsed_args.incident_changed=}, ") log(DEBUG, f"\n\n{parsed_args.inc_status=}, ") log(DEBUG, f"\n\n{parsed_args.remote_incident_id=}, ") log(DEBUG, f"\n\n{parsed_args.delta=}") if parsed_args.delta: log(DEBUG, f"Got the following delta keys {str(list(parsed_args.delta.keys()))}") remote_incident_id = parsed_args.remote_incident_id try: if parsed_args.incident_changed: issue_status: str = parsed_args.inc_status log(DEBUG, f"Incident {remote_incident_id} status changed to {issue_status}") if issue_status == IncidentStatus.DONE: log(DEBUG, f"Closing incident {remote_incident_id}") client.close_incident({"ticket_id": remote_incident_id, "t": datetime.now().timestamp()}) else: log(DEBUG, f"Modification to {remote_incident_id} is not configured for outgoing mirroring..") else: log(DEBUG, f"Incident {remote_incident_id} was not modified locally..") except DemistoException as e: if "Incident should be under Member Feedback or Monitoring to Close" in e.message: remote_incident = client.fetch_incident({"ticket_id": remote_incident_id}) if remote_incident.get("status") is not None and remote_incident.get("status") == "WIP": log(INFO, f"Incident {remote_incident_id} is undergoing a process..") return remote_incident_id def get_mapping_fields_command(): incident_type_scheme = SchemeTypeMapping(type_name="CyberBlindspot Incident") for field in INSTANCE.mapping_fields: incident_type_scheme.add_field(name=field["name"], description=field["description"]) return GetMappingFieldsResponse([incident_type_scheme]) def ctm360_cbs_list_command(client: Client, args: dict[str, Any]) -> CommandResults: if args.get("dateFrom"): args["dateFrom"] = convert_time_string(args["dateFrom"], CBS_OUTGOING_DATE_FORMAT, timestamp=True) if args.get("dateTo"): args["dateTo"] = convert_time_string(args["dateTo"], CBS_OUTGOING_DATE_FORMAT, timestamp=True) params = {to_snake_case(key): v for key, v in args.items()} params |= {"date_field": "@timestamp", "t": datetime.now().timestamp()} params |= {"module_type": INSTANCE.module} result = client.fetch_incidents(params) log(INFO, f"Received {len(result)} incidents") if len(result) > 0: result = [map_and_create_incident(item) for item in result] return CommandResults( outputs_prefix=INSTANCE.list_prefix, outputs_key_field="id", outputs=result, readable_output=tableToMarkdown(INSTANCE.list_prefix, result, headers=result[0].keys(), is_auto_json_transform=True) if len(result) > 0 else "No incidents within these dates", ) def ctm360_cbs_details_command(client: Client, args: dict[str, Any]) -> CommandResults: params = {to_snake_case(key): v for key, v in args.items()} params["t"] = datetime.now().timestamp() params |= {"module_type": INSTANCE.module} result = client.fetch_incident(params) log(INFO, f"Received {result}") if result.get("timestamp", ""): result["timestamp"] = str(normalize_timestamp(result["timestamp"])) return CommandResults( outputs_prefix=INSTANCE.details_prefix, outputs_key_field="id", outputs=result, readable_output=tableToMarkdown(INSTANCE.details_prefix, result, headers=result.keys(), is_auto_json_transform=True) if result.keys() else "No incident with that ID", ) def ctm360_cbs_incident_request_takedown_command(client: Client, args: dict[str, Any]) -> CommandResults: params = {to_snake_case(key): v for key, v in args.items()} result = client.request_takedown(params) msg = result.get("message", "") log(INFO, f'Request to takedown incident {args["ticketId"]} {"was " if result else "was un"}successful.') return CommandResults(readable_output=msg) def ctm360_cbs_incident_close_command(client: Client, args: dict[str, Any]) -> CommandResults: params = {to_snake_case(key): v for key, v in args.items()} result = client.close_incident(params) msg = result.get("message", "") log(INFO, f'Request to close incident {args["ticketId"]} {"was " if result else "was un"}successful.') return CommandResults(readable_output=msg) def ctm360_cbs_incident_retrieve_screenshots_command( client: Client, args: dict[str, Any] ) -> CommandResults | list[dict[str, Any]] | dict[str, Any]: """Get screenshot evidence for an incident Args: client (Client): CyberBlindspot client args (dict[str, Any]): Command arguments Returns: CommandResults | list[dict[str, Any]] | dict[str, Any]: File results or error message """ params = {to_snake_case(key): v for key, v in args.items()} log(DEBUG, f"Getting screenshot evidence for {params=}") try: # Early returns for disabled screenshots if not RETRIEVE_SCREENSHOTS: log(INFO, "Screenshot Evidence Retrieval is Disabled in Instance Configuration.") return CommandResults(readable_output="Screenshot Evidence Retrieval is Disabled in Instance Configuration.") # Get existing filenames from context existing_files = demisto.context().get("InfoFile", []) log(DEBUG, f"{existing_files=}") if not isinstance(existing_files, list): existing_files = [existing_files] if existing_files else [] existing_filenames = [d.get("Name") for d in existing_files if isinstance(d, dict) and d.get("Name")] # Filter requested files that already exist in context if "files" in params and isinstance(params["files"], list): original_files = params["files"] params["files"] = [ file_info for file_info in original_files if isinstance(file_info, dict) and file_info.get("filename") not in existing_filenames ] if not params["files"] and original_files: return CommandResults(readable_output="All requested screenshots already exist in context") # Make API call and handle errors try: results = client.get_screenshot_files(params) if params.get("files", True) else [] log(DEBUG, f"{results=}") except Exception as e: log(ERROR, f"Error calling get_screenshot_files: {str(e)}") return CommandResults(readable_output=f"Failed to fetch screenshots from API: {str(e)}") if not results: return CommandResults(readable_output="No new screenshots to fetch") # Process results file_results = [] for file_data in results: if not isinstance(file_data, dict): continue filename = file_data.get("filename") filedata = file_data.get("filedata", {}) if not filename or not isinstance(filedata, dict) or "data" not in filedata: continue try: data = bytes(filedata["data"]) file = fileResult(filename, data, file_type=EntryType.IMAGE) if file: file_results.append(file) except Exception as e: log(ERROR, f"Failed to process file {filename}: {str(e)}") continue log(DEBUG, f"{file_results=}") if not file_results: return CommandResults(readable_output="No new screenshots to add to context") log(INFO, f"Added {len(file_results)} new screenshot(s) to context") return file_results except Exception as e: log(ERROR, f"Failed to get screenshot evidence: {str(e)}") return CommandResults(readable_output=f"Failed to fetch screenshot(s): {str(e)}") """ MAIN FUNCTION """ def main() -> None: """main function, parses params and runs command functions :return: :rtype: """ try: log(DEBUG, "at main func") demisto_args = demisto.args() demisto_params = demisto.params() demisto_command = demisto.command() log(DEBUG, f"Command being called is {demisto_command}") log(DEBUG, f"Demisto Args are {demisto_args=}") client = Client( base_url=CBS_BASE_URL, verify=not demisto_params.get("insecure", False), headers={"api-key": demisto_params.get("api_key", {}).get("password")}, proxy=demisto_params.get("proxy", False), ) cbs_commands: dict[str, Any] = { "test-module": test_module, "get-mapping-fields": get_mapping_fields_command, "get-remote-data": get_remote_data_command, "get-modified-remote-data": get_modified_remote_data_command, "update-remote-system": update_remote_system_command, "ctm360-cbs-incident-list": ctm360_cbs_list_command, "ctm360-cbs-incident-details": ctm360_cbs_details_command, "ctm360-cbs-incident-request-takedown": ctm360_cbs_incident_request_takedown_command, "ctm360-cbs-incident-close": ctm360_cbs_incident_close_command, "ctm360-cbs-incident-retrieve-screenshots": ctm360_cbs_incident_retrieve_screenshots_command, } if demisto_command == "fetch-incidents": log(DEBUG, "at fetch-incidents command") last_run = demisto.getLastRun() last_fetch_ids = last_run.get("last_fetch_ids", []) params = build_fetch_params(demisto_params, last_run) log(INFO, f"Will be fetching up to {MAX_FETCH} records.") log(DEBUG, f"Calling fetch with the following: {params=}") log(DEBUG, f"Mirroring set as: {MIRROR_DIRECTION}") next_run, incidents = fetch_incidents(client, last_fetch_ids, params, last_run) log(DEBUG, f"Fetched {len(incidents)} incidents, {next_run=}") demisto.setLastRun(next_run) demisto.incidents(incidents) elif demisto_command == "test-module": return_results(test_module(client, demisto_params)) else: if getfullargspec(cbs_commands[demisto_command]).args: return_results(cbs_commands[demisto_command](client, demisto_args)) else: return_results(cbs_commands[demisto_command]()) # Log exceptions and return errors except Exception as e: return_error(f"Failed to execute {demisto.command()} command.\nError:\n{str(e)}", error=traceback.format_exc()) """ ENTRY POINT """ if __name__ in ("__main__", "__builtin__", "builtins"): main()