Group-IB Digital Risk Protection
Pack helps to integrate Group-IB Digital Risk Protection and get violations incidents directly into Cortex XSOAR.
Data Enrichment & Threat Intelligence · Group-IB Digital Risk Protection
Details
| ID | Group-IB Digital Risk Protection |
|---|---|
| Category | Data Enrichment & Threat Intelligence |
| From Version | 6.10.0 |
| Docker Image | demisto/vendors-sdk:1.0.0.10120494 |
README
Pack helps to integrate Group-IB Digital Risk Protection and get violations incidents directly into Cortex XSOAR.
This integration was integrated and tested with version 1.0 of Group-IB Digital Risk Protection.
Configure Group-IB Digital Risk Protection in Cortex
| Parameter | Description | Required |
|---|---|---|
| GIB DRP URL | True | |
| Fetch incidents | False | |
| Incident type | False | |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False | |
| Incidents Fetch Interval | False | |
| Username | True | |
| Password | True | |
| Violation Section to filter the received Violation | False | |
| Brands to filter the received Violation | Brands for filtering received violations. The list of available brands can be obtained with the command !gibdrp-get-brands in the menu WarRoom -> Playground. After getting the brands you must specify the brand ID for which you want to receive violations. Attention! Currently filtering is available only by one brand in one Instance | False |
| Incidents first fetch | Date to start fetching incidents from. | False |
| Download images | Enables or disables loading of each image in each violation. Can significantly affect the speed of data collection if the parameter is enabled, i.e. set to True | False |
| Getting Typosquatting only | Allows for the collection of offenses suitable only for Typo Squatting | False |
| Number of requests per collection | A number of requests per collection that integration sends in one fetch iteration (each request picks up to 30 incidents). If you face some runtime errors, lower the value. | True |
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.
gibdrp-get-brands
Receive all configured brands.
Base Command
gibdrp-get-brands
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
| Path | Type | Description |
|---|---|---|
| GIBDRP.OtherInfo | string | List of configured brands. |
| GIBDRP.OtherInfo.brands.name | string | Brand name. |
| GIBDRP.OtherInfo.brands.id | string | Brand ID. |
Command example
#### Context Example
```json
{
"GIBDRP": {
"OtherInfo": {
"brands": [
{
"id": "PvY1BZUBSFbLZGo2x8TA",
"name": "Example Brand"
}
]
}
}
}
Human Readable Output
Installed Brands
Name Id Example Brand PvY1BZUBSFbLZGo2x8TA
gibdrp-get-subscriptions
Receive all configured subscriptions.
Base Command
gibdrp-get-subscriptions
Input
| Argument Name | Description | Required |
| — | — | — |
Context Output
| Path | Type | Description |
|---|---|---|
| GIBDRP.OtherInfo | string | List of configured subscriptions. |
| GIBDRP.OtherInfo.subscriptions | string | Subscriptions list. |
Command example
#### Context Example
```json
{
"GIBDRP": {
"OtherInfo": {
"subscriptions": [
"scam"
]
}
}
}
Human Readable Output
Purchased subscriptions
Subscriptions scam
gibdrp-get-violation-by-id
Getting a single violation by its ID.
Base Command
gibdrp-get-violation-by-id
Input
| Argument Name | Description | Required |
|---|---|---|
| id | ID violation. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| GIBDRP.Violation.id | string | Violation ID. |
| GIBDRP.Violation.title | string | Violation title. |
| GIBDRP.Violation.description | string | Violation description. |
| GIBDRP.Violation.status | string | Violation status. |
| GIBDRP.Violation.violation_uri | string | Violation URI. |
| GIBDRP.Violation.source | string | Violation source section. |
| GIBDRP.Violation.detected | date | Detected timestamp. |
gibdrp-change-violation-status
Changing the status of a single violation.
Base Command
gibdrp-change-violation-status
Input
| Argument Name | Description | Required |
|---|---|---|
| id | ID violation. | Required |
| status | What status to change to. Possible values are: approve, reject. | Required |
Context Output
There is no context output for this command.
Configuration parameters
url— GIB DRP URL (required)isFetch— Fetch incidentsincidentType— Incident typeinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsincidentFetchInterval— Incidents Fetch Intervalcredentials— Username (required)violationSection— Filter by Violation Typebrands— Filter by Brandfirst_fetch— Incidents first fetchdownload_images— Download imagesonly_typosquatting— Get Typosquatting Onlymax_fetch— Number of requests per collection (required)
Commands (4)
-
gibdrp-change-violation-statusChanging the status of a single violation.
-
gibdrp-get-brandsReceive all configured brands.
-
gibdrp-get-subscriptionsReceive all configured subscriptions.
-
gibdrp-get-violation-by-idGetting a single violation by its ID.
import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * """ IMPORTS """ from urllib3.exceptions import InsecureRequestWarning from urllib3 import disable_warnings as urllib3_disable_warnings from cyberintegrations import DRPPoller from traceback import format_exc from enum import Enum from dateparser import parse as dateparser_parse # type: ignore from json import dumps as json_dumps import base64 from cyberintegrations.exception import ConnectionException from cyberintegrations.cyberintegrations import Parser from cyberintegrations.const import TechnicalConsts import re # Disable insecure warnings urllib3_disable_warnings(InsecureRequestWarning) """ CONSTANTS """ DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" class ViolationTypeMapping(Enum): WEB = 1 MARKETPLACE = 3 ADVERTISING = 5 MOBILE_APPS = 2 SOCIAL_NETWORKS = 4 INSTANT_MESSENGERS = 6 class ViolationSubType(Enum): Counterfeit = 1 Piracy = 2 Partner_policy_compliance = 3 Trademark = 4 Malware = 5 Phishing = 6 Fraud = 7 NoViolation = 8 COMMON_VIOLATION_MAPPING = { # Start Information From Group-IB DRP "id": "id", # GIB DRP ID "title": "violation.title", # GIB DRP Title "description": "violation.description", # GIB DRP Description "brand": "brand", # GIB DRP Brand "company": "company", # GIB DRP Company "violation_uri": "violation.uri", # GIB DRP VIOLATION URI "approve_state": "violation.approveState", # GIB DRP Approve State "violation_status": "violation.status", # GIB DRP Status "source": "violation.source", # GIB DRP Source "violation_type": "violation.violationSubtype", # GIB DRP Type "tags": "violation.tags.name", # GIB DRP Tags "link": "link", # GIB DRP Link "typosquatting_status": "*typosquatting_status", # GIB DRP Typosquatting Status # End Information From Group-IB DRP # Start Group-IB Dates "detected": "violation.detected", # GIB DRP Detected "first_detected": "violation.firstDetected", # GIB DRP First Detected "first_active": "violation.firstActive", # GIB DRP First Active "first_solved": "violation.firstSolved", # GIB DRP First Solved "dates_found_date": "violation.dates.foundDate", # GIB DRP Found "dates_created_date": "violation.dates.createdDate", # GIB DRP Created "dates_current_status_date": "violation.dates.currentStatusDate", # GIB DRP Current Status Date "dates_approved_date": "violation.dates.approvedDate", # GIB DRP Approved # End Group-IB Dates # Start Group-IB Images "images": "images", # GIB DRP HTML Images # End Group-IB Images # Start Group-IB Tables "scores": { # GIB DRP Scores Table "score": "violation.scores.score", "type": "violation.scores.type", "version": "violation.scores.version", }, # End Group-IB Tables } TABLES_MAPPING = ["scores", "stages"] STATUS_CODE_MSGS = { 401: "Bad Credentials", 403: "Something is wrong with your account, please, contact GIB.", 404: "Not found. There is no such data on server.", 500: "There are some troubles on server with your request.", 301: "Verify that your public IP is whitelisted by Group IB.", 302: "Verify that your public IP is whitelisted by Group IB.", } TIMEOUT = 360 RETRIES = 4 STATUS_LIST_TO_RETRY = [429, 500] class Endpoints(Enum): VIOLATIONS = "violation/list" VIOLATION = "violation" BRANDS = "/settings/brands" SUBSCRIPTIONS = "/settings/subscriptions" RECEIVING_FILE = "/file/" CHANGE_APPROVE = "/violation/change-approve" class Client(BaseClient): """ Client will implement the service API, and should not contain any Demisto logic. Should only do requests and return data. """ def __init__(self, base_url, auth: tuple[str, str], verify=True, proxy=False): super().__init__(base_url=base_url, verify=verify, proxy=proxy, auth=auth) self.poller = DRPPoller( username=auth[0], api_key=auth[1], api_url=base_url, ) self.poller.set_product( product_type="SOAR", product_name="CortexSOAR", product_version="unknown", integration_name="Group-IB Digital Risk Protection", integration_version="1.1.0", ) self.additional_headers = { "Accept": "*/*", "User-Agent": f"SOAR/CortexSOAR_unknown/Group-IB Digital Risk Protection/{auth[0]}", } def generate_seq_update(self, first_fetch_time: str) -> int: demisto.debug(f"Client.generate_seq_update: first_fetch_time='{first_fetch_time}'") date_from = dateparser_parse(date_string=first_fetch_time) if date_from is None: raise DemistoException( "Inappropriate first_fetch format, " f"please use a format such as: 2020-01-01 or January 1 2020 or 3 days. The format given is: {date_from}" ) date_from = date_from.strftime("%Y-%m-%d") demisto.debug(f"Client.generate_seq_update: date_from='{date_from}'") raw_seq = self.poller.get_seq_update_dict(date=date_from, collection=Endpoints.VIOLATIONS.value) demisto.debug(f"Client.generate_seq_update: raw_seq(type={type(raw_seq).__name__})={raw_seq!r}") # Verify the return type of get_seq_update_dict: # With `collection` provided, the get_seq_update_dict is expected to return an integer seqUpdate. if isinstance(raw_seq, int) and not isinstance(raw_seq, bool): return raw_seq raise DemistoException( "DRPPoller.get_seq_update_dict returned unexpected type. " "Expected int seqUpdate when 'collection' is provided. " f"Got type={type(raw_seq).__name__}, value={raw_seq!r}" ) def _get_violation_section_number(self, name: str) -> int: normalized_name = name.upper() normalized_name = normalized_name.replace(" ", "_") try: return ViolationTypeMapping[normalized_name].value except KeyError: raise ValueError(f"Unknown violation type: {name}") def create_generator( self, first_fetch_time: str, last_run: dict, only_typosquatting: bool, violation_subtypes: list[str] | None = None, brands: str | None = None, section: str | None = None, ): last_fetch = last_run.get("last_fetch", None) demisto.debug(f"Client.create_generator: last_fetch={last_fetch!r}") use_last_fetch = isinstance(last_fetch, int) and last_fetch > 0 sequpdate: int = last_fetch if use_last_fetch else self.generate_seq_update(first_fetch_time) demisto.debug( "Client.create_generator: sequpdate selection - " f"selected={sequpdate} source={'last_run.last_fetch' if use_last_fetch else 'generate_seq_update(first_fetch_time)'} " f"first_fetch_time={first_fetch_time!r}" ) if section: section: int = self._get_violation_section_number(section.strip()) # type: ignore[no-redef] if brands: brands = brands.strip(",") demisto.debug( "Client.create_generator: " f"collection={Endpoints.VIOLATIONS.value} subtypes={violation_subtypes} section={section} " f"sequpdate={sequpdate} brands={brands}" ) try: return self.poller.create_update_generator( collection_name=Endpoints.VIOLATIONS.value, subtypes=violation_subtypes, section=section, brands=brands, sequpdate=sequpdate, use_typo_squatting=only_typosquatting, ) except ConnectionException as e: raise ConnectionException( f"Additional information: collection_name: {Endpoints.VIOLATIONS.value} " f"subtypes: {violation_subtypes} section: {section} sequpdate: {sequpdate} {str(e)}" ) from e def change_violation_status(self, feed_id: str, status: str) -> int | str: """ Status could be approve or reject """ approve_statuses = {"approve": True, "reject": False} approve_status = approve_statuses.get(status) response = self.poller.search_feed_by_id(feed_id) demisto.debug( "Client.change_violation_status: " f"id={feed_id} approve={approve_status} " f"current_status={response.raw_dict.get('violation', {}).get('status')} " f"approve_state={response.raw_dict.get('violation', {}).get('approveState')}" ) violation_status = response.raw_dict.get("violation", {}).get("status", None) violation_approve_state = response.raw_dict.get("violation", {}).get("approveState", None) if violation_status == "detected" and violation_approve_state == "under_review": response = self._http_request( method="POST", url_suffix=Endpoints.CHANGE_APPROVE.value, timeout=TIMEOUT, retries=RETRIES, status_list_to_retry=STATUS_LIST_TO_RETRY, headers=self.additional_headers, json_data={"violationId": feed_id, "approve": approve_status}, resp_type="response", ) return response.status_code else: demisto.debug( "Client.change_violation_status: cannot change violation status due to current state - " f"id={feed_id} requested={status!r} " f"current_status={violation_status!r} approve_state={violation_approve_state!r} " "expected: status='detected' and approveState='under_review'" ) return "Can not change the status of the selected feed" def get_formatted_brands(self) -> list[dict[str, str]]: if hasattr(self.poller, "get_brands"): try: brands_data = self.poller.get_brands() # type: ignore[attr-defined] return brands_data or [] except Exception as e: demisto.debug(f"get_brands failed: {e}") return [] demisto.debug( "DRPPoller.get_brands is not available; returning empty list. " f"Library version: {TechnicalConsts.library_version}" ) return [] def get_formatted_subscriptions(self) -> list[str]: if hasattr(self.poller, "get_subscriptions"): try: return self.poller.get_subscriptions() or [] # type: ignore[attr-defined] except Exception as e: demisto.debug( f"DRPPoller.get_subscriptions exception ({type(e).__name__}): {e!s}. " f"poller={type(self.poller).__name__}, library_version={TechnicalConsts.library_version}. " "Returning empty list." ) return [] demisto.debug( "DRPPoller.get_subscriptions is not available; returning empty list. " f"Library version: {TechnicalConsts.library_version}" ) return [] def get_file(self, file_sha: str) -> tuple[bytes, str] | None: try: response = self._http_request( method="GET", url_suffix=Endpoints.RECEIVING_FILE.value + file_sha, timeout=TIMEOUT, retries=RETRIES, status_list_to_retry=STATUS_LIST_TO_RETRY, headers=self.additional_headers, resp_type="response", ) mime_type = CommonHelpers.extract_mime_type(response.headers.get("content-type", "")) content_len = len(response.content) if hasattr(response, "content") and response.content is not None else 0 status_code = getattr(response, "status_code", None) demisto.debug( "Client.get_file: downloaded file - " f"file_sha={file_sha} status_code={status_code} mime_type={mime_type} content_len={content_len}" ) data = response.content, mime_type except Exception as e: data = None demisto.debug( "Client.get_file: Could not download or the following image is not available - " f"file_sha={file_sha} error_type={type(e).__name__} error={e!s}\n{format_exc()}" ) return data def get_violation_by_id(self, violation_id: str) -> Parser: results = self.poller.search_feed_by_id(violation_id) return results def get_formatted_violation_by_id( self, violation_id: str, get_images: bool | None = True ) -> tuple[dict[Any, Any], list[dict[str, str | bytes]]]: results = self.get_violation_by_id(violation_id=violation_id) parse_result: dict[Any, Any] = results.parse_portion(keys=COMMON_VIOLATION_MAPPING, as_json=False)[0] updated_images = [] if get_images: images = parse_result.get("images", []) if images and len(images) > 0: for image in images: image_data_and_mime_type = self.get_file(file_sha=image) if image_data_and_mime_type is not None: image_data, mime_type = image_data_and_mime_type demisto.debug(f"Client.get_formatted_violation_by_id: image mime_type={mime_type}") updated_images.append( { "file_sha": image, "image_data": image_data, "mime_type": mime_type, } ) return parse_result, updated_images """ Support functions """ class CommonHelpers: scores_tables_name_by_types = { "risk": "General Score ", "domain": "Domain Score ", "image": "Image Score ", "parking": "Parking Score ", "text": "Text Score ", } @staticmethod def transform_dict(input_dict: dict[str, list[str | list[Any]] | str | None]) -> list[dict[str, Any]]: if not input_dict: return [{}] normalized_dict: dict[str, list[Any]] = {} for k, v in input_dict.items(): if isinstance(v, list): normalized_dict[k] = v elif v is None: normalized_dict[k] = [] else: normalized_dict[k] = [v] max_length = max((len(v) for v in normalized_dict.values() if isinstance(v, list)), default=1) result = [] for i in range(max_length): result.append({k: (v[i] if i < len(v) else (v[0] if v else None)) for k, v in normalized_dict.items()}) return result @staticmethod def transform_additional_fields_to_markdown_tables(feed: dict): additional_tables = [] delete_keys = [] for key, value in feed.items(): if key == "scores" and isinstance(value, dict): additional_data = CommonHelpers.transform_dict(value) position_score_dict = [item for item in additional_data if item.get("type") == "position"][0] additional_data = [item for item in additional_data if item.get("type") != "position"] for item in additional_data: value_type = item.get("type") table_name = "Table" if value_type == "risk": item.update( { "position_score": position_score_dict.get("score"), "position_version": position_score_dict.get("version"), } ) elif value_type is not None: table_name = CommonHelpers.scores_tables_name_by_types.get(value_type, "Table") table = CommonHelpers.get_human_readable_feed( table=item, name=table_name, ) additional_tables.append( CommandResults( readable_output=table, ignore_auto_extract=True, ) ) delete_keys.append(key) elif isinstance(value, dict): additional_data = CommonHelpers.transform_dict(value) for index, item in enumerate(additional_data): table = CommonHelpers.get_human_readable_feed(table=item, name=f"{key} table {index}") additional_tables.append( CommandResults( readable_output=table, ignore_auto_extract=True, ) ) delete_keys.append(key) for key in delete_keys: feed.pop(key) return feed, additional_tables @staticmethod def get_human_readable_feed(table: dict[Any, Any], name: str): return tableToMarkdown( name=name, t=table, removeNull=True, ) @staticmethod def get_table_data( feed: dict[Any, Any], ): updated_feed, additional_tables = CommonHelpers.transform_additional_fields_to_markdown_tables(feed) return updated_feed, additional_tables @staticmethod def violation_source_mapping(feed: dict) -> dict: source = feed.get("source") feed["source"] = ViolationTypeMapping(source).name return feed @staticmethod def convert_iso8601_with_timezone(date_str: str): if not re.match(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\+\d{4}$", date_str): raise ValueError("Invalid date format. A string in the following format is expected 'YYYY-MM-DDTHH:MM:SS+0000'.") date_part = date_str[:19] # '2024-10-30T15:12:34' timezone_part = date_str[19:] # '+0000' formatted_timezone = f"{timezone_part[:3]}:{timezone_part[3:]}" # '+00:00' formatted_date = f"{date_part}{formatted_timezone}" return formatted_date @staticmethod def format_dates_in_dict(data: dict): date_keys = [ "dates_created_date", "dates_found_date", "dates_approved_date", "dates_current_status_date", "datetime", "first_detected", "first_active", "first_solved", "detected", "stages", ] for key, value in data.items(): if key in date_keys and value is not None: if isinstance(value, str): data[key] = CommonHelpers.convert_iso8601_with_timezone(value) elif isinstance(value, dict): CommonHelpers.format_dates_in_dict(value) elif isinstance(value, list): data[key] = list(map(CommonHelpers.convert_iso8601_with_timezone, value)) return data @staticmethod def all_lists_empty(data: dict[str, Any] | list[Any]) -> bool: all_empty = True if isinstance(data, dict): for value in data.values(): if isinstance(value, list): if value: all_empty = False elif isinstance(value, dict) and not CommonHelpers.all_lists_empty(value): all_empty = False elif isinstance(data, list): for item in data: if isinstance(item, dict) and not CommonHelpers.all_lists_empty(item): all_empty = False return all_empty @staticmethod def replace_empty_values(data: dict[str, Any] | list[dict[str, Any]]) -> dict[str, Any] | list[dict[str, Any]]: if isinstance(data, dict): return {key: CommonHelpers.replace_empty_values(value) for key, value in data.items()} elif isinstance(data, list): if not data: return None # type: ignore if all(isinstance(item, list) and not item for item in data): return None # type: ignore return [CommonHelpers.replace_empty_values(item) for item in data] # type: ignore else: if data == "": return None return data @staticmethod def remove_underscore_and_lowercase_keys(dict_list: list[dict[str, Any]] | list[dict[str, Any]]) -> list[dict[str, Any]]: updated_dicts = [] for d in dict_list: new_dict = {} for key, value in d.items(): new_key = key.replace("_", "").lower() new_dict[new_key] = value updated_dicts.append(new_dict) return updated_dicts @staticmethod def data_pre_cleaning(violation: dict[str, Any]) -> dict[str, Any]: demisto.debug( f"CommonHelpers.data_pre_cleaning start: keys={list(violation.keys())}, " f"uri='{violation.get('violation_uri', '')}'" ) violation_uri: str = violation.get("violation_uri", "") if violation_uri.startswith("//"): violation_uri = violation_uri[2:] violation["violation_uri"] = violation_uri tags = violation.get("tags") if tags: tags = [item for item in tags if item is not None] violation["tags"] = tags demisto.debug( f"CommonHelpers.data_pre_cleaning done: uri='{violation_uri}', " f"tags_count={len(tags) if isinstance(tags, list) else 0}, keys={list(violation.keys())}" ) return violation @staticmethod def extract_mime_type(content_type: str) -> str: match = re.match(r"^\s*([^;]+)", content_type) return match.group(1).strip() if match else "image/jpeg" @staticmethod def set_tag_downloaded_by_typoSquatting(violation: dict[str, Any], only_typosquatting: bool) -> dict[str, Any]: if violation.get("typosquatting_status", None) and only_typosquatting: violation["typosquatting_status"] = True else: violation["typosquatting_status"] = False return violation class IncidentBuilder: def __init__( self, client: Client, last_run: dict, first_fetch_time: str, max_requests: int, download_images: bool, only_typosquatting: bool, violation_subtypes: list[str] | None, violation_section: str | None, brands: str | None, ) -> None: self.client = client self.last_run = last_run self.first_fetch_time = first_fetch_time self.max_requests = max_requests self.violation_subtypes = violation_subtypes self.violation_section = violation_section self.brands = brands self.download_images = download_images self.only_typosquatting = only_typosquatting def transform_fields_to_grid_table(self, incident: dict): if TABLES_MAPPING: for field in TABLES_MAPPING: field_data = incident.get(field, {}) if field_data and CommonHelpers.all_lists_empty(field_data) is False: transformed_data = CommonHelpers.transform_dict(input_dict=field_data) transformed_and_replaced_empty_values_data = CommonHelpers.replace_empty_values(transformed_data) clean_data = CommonHelpers.remove_underscore_and_lowercase_keys( transformed_and_replaced_empty_values_data # type: ignore ) if field == "scores": clean_data = [item for item in clean_data if item["type"] != "position"] for score in clean_data: score_type = score.get("type") if isinstance(score_type, str): score["type"] = CommonHelpers.scores_tables_name_by_types.get(score_type, "Unknown") else: score["type"] = "Unknown" demisto.debug( "IncidentBuilder.transform_fields_to_grid_table: scores normalized " f"(count={len(clean_data)}, types={[item.get('type') for item in clean_data]})" ) incident[field] = clean_data else: incident[field] = None return incident def build(self) -> tuple[dict[str, int | Any], list]: previous_last_fetch = self.last_run.get("last_fetch") next_run: dict[str, int | Any] = {"last_fetch": previous_last_fetch} violations: list[dict[str, Any]] = [] requests_count = 0 max_seq_update: int | None = None portions = self.client.create_generator( violation_subtypes=self.violation_subtypes, section=self.violation_section, brands=self.brands, first_fetch_time=self.first_fetch_time, last_run=self.last_run, only_typosquatting=self.only_typosquatting, ) for portion in portions: portion_sequpdate = getattr(portion, "sequpdate", None) demisto.debug( "IncidentBuilder.build: processing portion - " f"requests_count={requests_count} max_requests={self.max_requests} " f"portion_sequpdate={portion_sequpdate!r}" ) sequpdate = portion.sequpdate parse_result: list[dict[Any, Any]] = portion.parse_portion(keys=COMMON_VIOLATION_MAPPING, as_json=False) demisto.debug( "IncidentBuilder.build: portion parsed - " f"portion_sequpdate={sequpdate!r} parsed_items={len(parse_result)}" ) created_before = len(violations) max_seq_before = max_seq_update for feed in parse_result: feed = CommonHelpers.data_pre_cleaning(violation=feed) feed = CommonHelpers.violation_source_mapping(feed=feed) feed = CommonHelpers.format_dates_in_dict(data=feed) feed = CommonHelpers.set_tag_downloaded_by_typoSquatting( violation=feed, only_typosquatting=self.only_typosquatting ) incident = self.transform_fields_to_grid_table(incident=feed) if self.download_images: images = incident.get("images", []) updated_images = [] if images and len(images) > 0: for image in images: image_data = self.client.get_file(file_sha=image) if image_data: image_bytes, mime_type = image_data demisto.debug(f"IncidentBuilder.build: image mime_type={mime_type}") image_base64_uri = f"data:{mime_type};base64,{base64.b64encode(image_bytes).decode('utf-8')}" image_html = f'<img src="{image_base64_uri}" alt="Violation Incident Image" />' updated_images.append(image_html) if len(updated_images) > 0: incident["images"] = "<br/>".join(updated_images) else: incident.pop("images") else: incident.pop("images") # Prefer standard fields: name from title, occurred from created date (fallback to detected) preferred_name = incident.get("title") or f"Violation {incident.get('id')}" preferred_occurred = incident.get("dates_created_date") or incident.get("detected") incident.update( { "name": preferred_name, "occurred": preferred_occurred, "gibType": Endpoints.VIOLATIONS.value, } ) violations.append( { "name": incident.get("name"), "occurred": incident.get("occurred"), "rawJSON": json_dumps(incident), "dbotMirrorId": incident.get("id"), } ) # Track the highest seqUpdate seen in this run try: if isinstance(sequpdate, int): current_max = max_seq_update if isinstance(max_seq_update, int) else 0 max_seq_update = max(sequpdate, current_max) except Exception as e: demisto.debug( "IncidentBuilder.build: failed to compare/track seqUpdate; skipping. " f"sequpdate={sequpdate!r} max_seq_update={max_seq_update!r} " f"error_type={type(e).__name__} error={e!s}\n{format_exc()}" ) requests_count += 1 created_after = len(violations) demisto.debug( "IncidentBuilder.build: portion done - " f"portion_sequpdate={sequpdate!r} created_in_portion={created_after - created_before} " f"max_seq_update_before={max_seq_before!r} max_seq_update_after={max_seq_update!r} " f"requests_count={requests_count} max_requests={self.max_requests}" ) if requests_count > self.max_requests: demisto.debug( "IncidentBuilder.build: stopping due to max_requests limit - " f"requests_count={requests_count} max_requests={self.max_requests} " f"last_portion_sequpdate={sequpdate!r} max_seq_update={max_seq_update!r}" ) break # Decide effective next_run.last_fetch effective_last = previous_last_fetch if isinstance(max_seq_update, int) and max_seq_update > 0: if isinstance(previous_last_fetch, int) and previous_last_fetch > 0: effective_last = max(previous_last_fetch, max_seq_update) else: effective_last = max_seq_update else: demisto.debug( "IncidentBuilder.build: not updating last_fetch because no seqUpdate was observed - " f"previous_last_fetch={previous_last_fetch!r} max_seq_update={max_seq_update!r} " f"requests_count={requests_count} created_incidents={len(violations)}" ) next_run["last_fetch"] = effective_last demisto.debug( "IncidentBuilder.build: " f"computed next_run.last_fetch={effective_last} " f"(prev={previous_last_fetch}, max_seq={max_seq_update})" ) return next_run, violations class BuilderCommandResponses: def __init__( self, requested_method: str, client: Client, args: dict, first_fetch: str, max_requests: int, ) -> None: self.requested_method = requested_method self.client = client self.args = args self.first_fetch = first_fetch self.max_requests = max_requests def get_brands(self) -> CommandResults: response_result = self.client.get_formatted_brands() readable_output = tableToMarkdown( name="Installed Brands", t=response_result, headers=["name", "id"], headerTransform=lambda x: x.capitalize(), ) return CommandResults( outputs_prefix="GIBDRP.OtherInfo", outputs_key_field="id", outputs={"brands": response_result}, readable_output=readable_output, ignore_auto_extract=True, raw_response=response_result, ) def get_subscriptions(self) -> CommandResults: response_result = self.client.get_formatted_subscriptions() readable_output = tableToMarkdown( name="Purchased subscriptions", t=response_result, headers="Subscriptions", ) return CommandResults( outputs_prefix="GIBDRP.OtherInfo", outputs_key_field="subscriptions", outputs={"subscriptions": response_result}, readable_output=readable_output, ignore_auto_extract=True, raw_response=response_result, ) def get_violation_by_id(self) -> list[CommandResults]: """ the returned list has dict[str, Any], which is fileResult. And an important note, not necessarily picture files, i.e. fileResults definitely will be. """ id_ = str(self.args.get("id")) parse_result, updated_images = self.client.get_formatted_violation_by_id(violation_id=id_) parse_result: dict = CommonHelpers.data_pre_cleaning(violation=parse_result) parse_result = CommonHelpers.violation_source_mapping(feed=parse_result) updated_feed, additional_tables = CommonHelpers.get_table_data(feed=parse_result) readable_output = CommonHelpers.get_human_readable_feed(table=updated_feed, name=f"Feed {id_}") results = [] results.append( CommandResults( outputs_key_field="id", outputs=updated_feed, readable_output=readable_output, raw_response=updated_feed, ignore_auto_extract=True, ) ) results.extend(additional_tables) if updated_images: for updated_image in updated_images: results.append( fileResult( filename=f"Attached image {updated_image.get('file_sha', 'default')}", # type: ignore data=updated_image.get("image_data", ""), ) ) return results def change_violation_status(self) -> str: id_ = str(self.args.get("id")) status = str(self.args.get("status")) result = self.client.change_violation_status(feed_id=id_, status=status) demisto.debug(f"change_violation_status {id_} {status} {result}") if result == 200: return "Request to change violation status sent" else: return result # type: ignore[return-value] def build(self) -> str | tuple[CommandResults, dict[str, Any]] | CommandResults: # Check if the method exists in the class if hasattr(self, self.requested_method) and callable(getattr(self, self.requested_method)): # Call the method return getattr(self, self.requested_method)() else: raise AttributeError(f"Method {self.requested_method} is not implemented.") """ Commands """ class Commands: methods_requiring_return_results = { "get_brands", "get_subscriptions", "get_violation_by_id", "change_violation_status", "test_module", } def __init__( self, client: Client, command: str, args: dict, first_fetch: str, max_requests: int, download_images: bool, only_typosquatting: bool, violation_subtypes: list[str] | list | None, violation_section: str | None = None, brands: str | None = None, ) -> None: self.client = client self.command = command self.args = args self.first_fetch = first_fetch self.max_requests = max_requests self.last_run = demisto.getLastRun() self.requested_method = self.command.replace("gibdrp-", "").replace("-", "_") self.violation_subtypes = violation_subtypes self.violation_section = violation_section self.brands = brands self.download_images = download_images self.only_typosquatting = only_typosquatting def get_brands(self) -> str | tuple[CommandResults, dict[str, Any]] | CommandResults: results = BuilderCommandResponses( self.requested_method, self.client, self.args, self.first_fetch, self.max_requests, ).build() return results def get_subscriptions(self) -> str | tuple[CommandResults, dict[str, Any]] | CommandResults: results = BuilderCommandResponses( self.requested_method, self.client, self.args, self.first_fetch, self.max_requests, ).build() return results def get_violation_by_id(self) -> str | tuple[CommandResults, dict[str, Any]] | CommandResults: results = BuilderCommandResponses( self.requested_method, self.client, self.args, self.first_fetch, self.max_requests, ).build() return results def change_violation_status(self) -> str | tuple[CommandResults, dict[str, Any]] | CommandResults: results = BuilderCommandResponses( self.requested_method, self.client, self.args, self.first_fetch, self.max_requests, ).build() return results def test_module(self) -> str: response = self.client.get_formatted_brands() if isinstance(response, list) and len(response) > 0: return "ok" return "Test failed, some problems with getting brands." def fetch_incidents( self, ) -> tuple[dict[str, int | Any], list]: next_run, violations = IncidentBuilder( client=self.client, last_run=self.last_run, first_fetch_time=self.first_fetch, max_requests=self.max_requests, violation_subtypes=self.violation_subtypes, violation_section=self.violation_section, brands=self.brands, download_images=self.download_images, only_typosquatting=self.only_typosquatting, ).build() return next_run, violations @staticmethod def get_avalible_commands(): """ Returns a list of available commands. Adds the prefix `gibdrp-` to all methods except `test_module` and `fetch_incidents`. """ def format_method(method_name: str) -> str | None: # Exclude magic methods and the get_avalible_commands and get_results functions themselves if method_name.startswith("__") or method_name in ( "get_avalible_commands", "get_results", ): return None # Exceptions: methods without a prefix but with hyphens if method_name in ["test_module", "fetch_incidents"]: return method_name.replace("_", "-") # For the others, add a prefix and replace `_` with `-` return f"gibdrp-{method_name.replace('_', '-')}" # Get and format all methods methods = [format_method(method) for method in dir(Commands) if callable(getattr(Commands, method))] return list(filter(None, methods)) def get_results( self, ) -> ( tuple[CommandResults, str] | tuple[tuple[CommandResults, tuple[dict, list]], str] | tuple[str, str] | tuple[tuple[dict[str, int | Any], list], str] ): # Check if the method exists in the class if hasattr(self, self.requested_method) and callable(getattr(self, self.requested_method)): # Call the method return getattr(self, self.requested_method)(), self.requested_method else: raise AttributeError(f"Command {self.command} is not implemented.") def main(): """ PARSE AND VALIDATE INTEGRATION PARAMS """ try: params = demisto.params() args = demisto.args() command = demisto.command() username, api_token = ( params.get("credentials", {}).get("identifier", ""), params.get("credentials", {}).get("password", ""), ) base_url = str(params.get("url")) proxy = params.get("proxy", False) verify_certificate = not params.get("insecure", False) first_fetch = params.get("first_fetch", "3 days").strip() max_requests = int(params.get("max_fetch", 3)) # violation_subtypes = params.get("violationSubtypes") # At the moment this # filtering moment is not valid, # it can be used once the cyberintegrations library is updated violation_subtypes = None violation_section = params.get("violationSection") # Currently all brands are transferred, but filtering will only happen on the very # first brand because of the filtering in the library, # but after time it is planned to update the library and then it will be possible # to filter on multiple brands brands = params.get("brands") download_images = params.get("download_images", False) only_typosquatting = params.get("only_typosquatting", False) client = Client( base_url=base_url, verify=verify_certificate, auth=(username, api_token), proxy=proxy, ) # All available commands are described here, but this # particular array is not used in the code in any way, # because the commands are implemented flexibly and # such a record of them is required only for passing the tests if command in [ "test-module", "gibdrp-get-brands", "gibdrp-get-subscriptions", "gibdrp-get-violation-by-id", "gibdrp-change-violation-status", ]: pass if command not in Commands.get_avalible_commands(): raise Exception(f"{command} invalid") results, requested_method = Commands( client=client, command=command, args=args, first_fetch=first_fetch, max_requests=max_requests, violation_subtypes=violation_subtypes, violation_section=violation_section, brands=brands, download_images=download_images, only_typosquatting=only_typosquatting, ).get_results() if requested_method in Commands.methods_requiring_return_results: return_results(results) else: if isinstance(results, tuple) and len(results) == 2: next_run, violations = results demisto.setLastRun(next_run) demisto.incidents(violations) else: raise ValueError("Expected results to be a tuple containing next_run and violations") except Exception: return_error(f"Failed to execute {demisto.command()} command.\n" f"Error: {format_exc()}") if __name__ in ("__main__", "__builtin__", "builtins"): main()