Group-IB Threat Intelligence & Attribution

Pack helps to integrate Group-IB Threat Intelligence and get incidents directly into Cortex XSOAR. The list of included collections: Compromised Accounts, Compromised Cards, Compromised Masked Cards, Brand Protection Phishing, Brand Protection Phishing Kit, OSI Git Leak, OSI Public Leak, Targeted Malware.

Data Enrichment & Threat Intelligence · Group-IB Threat Intelligence

Details

IDGroup-IB Threat Intelligence & Attribution
ProviderGroup IB
CategoryData Enrichment & Threat Intelligence
From Version6.0.0
Docker Imagedemisto/vendors-sdk:1.0.0.10120494
Supported ModulesAgentix XSIAM

README

Group-IB Threat Intelligence

Pack helps to integrate Group-IB Threat Intelligence and get incidents directly into Cortex XSOAR.
The integration supports multiple collections including compromised accounts, bank cards, breaches, malware, attacks, OSI leaks, vulnerabilities, and threat intelligence. See the Data Collections Overview section below for the complete list with descriptions and recommended date ranges.

Prerequisites

  1. Access Group-IB Threat Intelligence (TI) Web Interface
  2. Generate API Credentials
    • In the web interface, click your name in the upper right corner
    • Select ProfileSecurity and Access tab
    • Click Personal token and follow the instructions to generate your API token
    • Note: The API token serves as your password for authentication
  3. Network Configuration
    • Important: Contact Group-IB support to add your Cortex XSOAR server’s IP address to the allow list
    • If you are using a proxy, provide the public IP address of the proxy server instead
    • Make sure you have added Group-IB API IPs/URLs to you FW/Proxy rules.

Important Notes

Recommended Instance Layout

Run separate integration instances for the following groups:

  • Accounts unique
  • Accounts combolist
  • Cards (masked/unmasked)
  • Public leaks/Git Leaks
  • Breached
  • Vulnerabilities
  • Malware reports and threats including profiles
  • SPD
  • Suspicious IP
  • Malware CNC
  • DDoS/Deface/phishing/phishing_kit

Limit Parameter

The Limit (items per request) parameter specifies the number of records requested per API page. This limit applies to all collections configured in the integration instance.

Important considerations:

  • The limit determines how many records are fetched in a single API request. For example, if “Number of requests per collection” is set to 2 and the limit is 500, the integration will make 2 requests per collection, each requesting up to 500 records, resulting in up to 1000 records per collection per fetch cycle.
  • Different collections may have different optimal limit values based on their data structure and API recommendations. We strongly recommend consulting the official API Limitations documentation for specific limit recommendations for each collection.
  • Best practice: Create separate integration instances for different collections or groups of collections that share similar optimal limit values. This allows you to optimize performance for each collection type.

Collection-Specific Filters

The following three filters control data collection behavior for the compromised/account_group collection:

  • Include unique type in data: Filter to include unique data from the compromised/account_group collection
  • Include combolist type in data: Filter to include combolist data from the compromised/account_group collection
  • Enable filter “Probable Corporate Access”: Filter to limit data collection to only corporate accounts

Filter Logic (applies to unique and combolist filters):

  • If both Include unique type in data and Include combolist type in data are disabled: No filtering is applied, and both types of data are collected
  • If only Include unique type in data is enabled: Only unique records are collected
  • If only Include combolist type in data is enabled: Only combolist records are collected
  • If both Include unique type in data and Include combolist type in data are enabled: Both types of data are collected
  • When both unique and combolist filters are not enabled (no checkboxes selected): Both unique and combolist data types are collected by default (as stated above). In this state, you can enable Enable filter "Probable Corporate Access" to limit the entire feed (both unique and combolist data) to only corporate accounts. You can also combine the corporate access filter with unique or combolist filters, if needed. For example, if you are collecting only combolist data (without unique), you can enable Enable filter "Probable Corporate Access" to limit the combolist collection to only corporate accounts

Best Practice: For optimal organization and performance, consider running two separate integration instances:

  • Instance 1: Enable Include unique type in data only
  • Instance 2: Enable Include combolist type in data only
  • Instance 3 (optional): Enable ‘Probable Corporate Access’ - if you need to focus on your company employees compromises only

These filters have no effect on other collections.

Data Collections Overview

Once the configuration is complete, the following collections become available in Cortex XSOAR. For detailed information about each collection, its structure, and available fields, please refer to the official Collections Details documentation.

Note: If you’re using a POC or partner license, access to data is limited to 30 days. The recommended date ranges below are guidelines and can be adjusted according to your needs.

Collection Description Recommended Date Range
compromised/account_group The collection contains credentials collected from various phishing resources, botnets, C&C servers, Darkweb, etc., used by hackers. All indicated sources are unique and private. It also includes combolist and corporate accounts. For Public Breaches - please refer to compromised/breached. 2-4 years
compromised/bank_card_group Information about compromised bank cards, sourced from card shops, forums, and public leaks. 2 years
compromised/masked_card Information about compromised masked bank card records returned as individual card entries, including card, owner, malware, source, and CNC context. 2 years
compromised/mule Information on compromised accounts used by threat actors for money laundering and fund transfers. 90 days
compromised/spd Suspicious payment details collected from underground markets, forums, and messaging platforms. 90 days
compromised/breached Information about publicly leaked databases containing credentials and personal data. Note: Hunting rules are on by default here. 90 days
attacks/ddos Data on Distributed Denial of Service (DDoS) attacks, including targeted resources and attack durations. 5-10 days
attacks/deface Records of defacement attacks, highlighting compromised websites and related actors. 5-10 days
attacks/phishing_group Information on phishing attacks, including URLs of phishing websites. Note: Do not use IPs for detection - it may cause many false positives. Focus only on URLs. 3-5 days
attacks/phishing_kit Collections of phishing website templates, scripts, and configurations used by attackers. 30 days
apt/threat Reports on nation-state APTs activities, including associated indicators (IOCs), attack techniques, and MITRE ATT&CK mappings. 2-4 years
apt/threat_actor Profiles of nation-state groups detailing their characteristics, targets, motivations, and techniques. 2-4 years
hi/threat Finance motivated cybercriminals reports, including associated indicators (IOCs), attack techniques, and MITRE ATT&CK mappings. 2-4 years
hi/threat_actor Profiles of financially motivated cybercriminals detailing their characteristics, targets, motivations, and techniques. 2-4 years
malware/cnc Information on malware Command-and-Control (C&C) servers used for data exfiltration and command distribution. 90 days
malware/malware Detailed malware descriptions. 2-4 years
osi/git_repository Publicly available code from repositories like GitHub, filtered by your hunting rules. Note: Hunting rules are on by default here. 30 days
osi/public_leak Public data leaks from sources like Pastebin, ghostbin, and others, including credentials, database dumps, configuration files, and logs. Note: Hunting rules are on by default here. 15 days
osi/vulnerability Information on software vulnerabilities, associated exploits, and available proof-of-concept details. 90 days
suspicious_ip/tor_node Data about known Tor exit nodes used as anonymity relays. 5 days
suspicious_ip/open_proxy Information on publicly available proxy servers, including potentially misconfigured proxies. 5 days
suspicious_ip/scanner IP addresses identified as scanning or probing corporate networks. 5 days
suspicious_ip/socks_proxy IP addresses of infected hosts configured as SOCKS proxies used for anonymized attacks. 5 days
suspicious_ip/vpn Information about public and private VPN servers identified as potentially malicious or suspicious. 5 days

Configure Group-IB Threat Intelligence in Cortex

Parameter Description Required
GIB TI URL The FQDN/IP the integration should connect to (default: https://tap.group-ib.com/api/v2/). True
Username Enter the email address you use to log into the web interface. The API token serves as your password for authentication. True
Trust any certificate (not secure) Whether to allow connections without verifying SSL certificates validity. False
Use system proxy settings Whether to use XSOAR system proxy settings to connect to the API. False
Source Reliability Reliability of the source providing the intelligence data. Used as a fixed reliability for reputation commands unless overridden by Ignore Source Reliability override. True
Ignore Source Reliability override If enabled, ignore the instance Source Reliability setting and use the integration’s computed reliability per indicator for reputation commands. False
Colletions to fetch Select the collections you want to fetch incidents from. Read more about collections here. False
Incidents first fetch Specify the date range for initial data fetch (default: “3 days”). False
Number of requests per collection Number of API requests per collection in each fetch iteration (default: 3). If you face some runtime errors, lower the value. False
Skip updated incidents (prevent duplicates) Disabled by default. Enable this only when you want the integration itself to suppress duplicate incidents because Pre-Processing Rules are not working reliably in your environment. When enabled, the integration skips Group-IB records that were already fetched and later re-sent after updates. False
Deduplication lookback (days) Used only when Skip updated incidents (prevent duplicates) is enabled. Defines how long fetched Group-IB incident IDs are remembered in the built-in deduplication cache. Recommended value is 365 days. False
Limit (items per request) Number of items requested per API page. This limit applies to all collections in the instance. The limit determines how many records are fetched in a single API request. For example, if “Number of requests per collection” is 2 and limit is 500, the integration will make 2 requests per collection, each requesting up to 500 records, resulting in up to 1000 records per collection per fetch cycle. We recommend following the official API Limitations documentation for collection-specific limit recommendations. Best practice: create separate integration instances for different collections or groups of collections with similar optimal limit values. False
Enable reputation commands Multi-select list of reputation commands to enable for this integration instance (supported: ip, domain, file). Default: none enabled (fail-safe). Only selected commands perform enrichment and return DBotScore. False
Include combolist type in data Filter to include combolist data from the compromised/account_group collection. Works only for compromised/account_group collection. Filter logic: If only this filter is enabled, only combolist records are collected. If both combolist and unique filters are enabled, both types are collected. If both are disabled, both types are collected by default. False
Include unique type in data Filter to include unique data from the compromised/account_group collection. Works only for compromised/account_group collection. Filter logic: If only this filter is enabled, only unique records are collected. If both combolist and unique filters are enabled, both types are collected. If both are disabled, both types are collected by default. False
Enable filter “Probable Corporate Access” Filter to limit data collection to only corporate accounts. Works only for compromised/account_group collection. When both unique and combolist filters are not enabled, you can enable this to limit the whole feed to corporate accounts only. Can also be combined with unique or combolist filters if needed. False
Hunting Rules To enable the collection of data using hunting rules, please select this parameter. False

Note

Requests to the following collections come with the Hunting Rules parameter by default - and turing it off or on won’t make any changes: osi/git_repository, osi/public_leak, compromised/breached, compromised/messenger, compromised/discord

Built-in deduplication should be enabled only when Pre-Processing Rules are not working reliably in your environment. If you rely on Pre-Processing Rules to update existing incidents, keep Skip updated incidents (prevent duplicates) disabled.

Additional Resources

For detailed information about collections, their structure, available fields, and recommended date ranges, refer to the official Collections Details documentation.

Reputation Commands (ip / domain / file)

This integration implements the standard Cortex XSOAR reputation commands:

  • ip
  • domain
  • file

Best practice: use a dedicated instance for reputation

We recommend using a dedicated integration instance for reputation commands, such as Group-IB Threat Intelligence (Partner Contribution).

Enabling reputation commands

Reputation commands are disabled by default to avoid unexpected auto-enrichment side effects.
To enable them, configure the integration instance parameter Enable reputation commands and select the command types you want to allow (ip, domain, file).

Source Reliability and override behavior

The integration supports two reliability modes for reputation commands:

  • Instance override mode (fixed reliability):
    • Controlled by the instance parameter Source Reliability.
    • When Ignore Source Reliability override is disabled (unchecked), the integration attaches the configured Source Reliability value to every reputation response, regardless of indicator-specific findings.
  • Integration-calculated reliability mode (dynamic reliability):
    • Enabled by the instance parameter Ignore Source Reliability override.
    • When Ignore Source Reliability override is enabled (checked), the integration ignores the instance Source Reliability value and calculates reliability per indicator based on the collections that returned matches (see rules below).

Score (DBotScore) calculation rules

Score and reliability are calculated independently. A finding may affect reliability without affecting score.

file score rules

  • BAD: at least one match in ioc/common
  • UNKNOWN (NONE): no matches

Note: For file reputation, the integration evaluates only the ioc/common collection for score.

domain score rules

The integration uses a 3-year recency window and the following date fields:

  • ioc/common.dateLastSeen
  • hi/open_threats.detected
  • attacks/deface.date

Rules (evaluated top-to-bottom):

  • BAD: ioc/common match with dateLastSeen within the last 3 years
  • SUSPICIOUS: hi/open_threats or attacks/deface match with a date within the last 3 years
  • SUSPICIOUS: ioc/common has records but dateLastSeen is missing or older than 3 years
  • UNKNOWN (NONE): no findings (no matches in ioc/common, hi/open_threats, attacks/deface)

ip score rules

The integration maps the numeric Group-IB riskScore (0..100) to DBotScore:

  • GOOD: 0..49
  • SUSPICIOUS: 50..84
  • BAD: 85..100
  • UNKNOWN (NONE): score is missing or out of range

Reliability calculation rules (only when Ignore Source Reliability override is enabled)

When the integration-calculated reliability mode is enabled, reliability is computed as follows:

file reliability rules

  • A - Completely reliable: at least one match in ioc/common
  • None: no matches

domain and ip reliability rules

Reliability is derived from which collections returned matches:

  • A - Completely reliable:
    • any match in apt/threat or apt/threat_actor (nation-state intelligence), or
    • any match in ioc/common
  • B - Usually reliable:
    • any match in attacks/deface, or
    • any match in hi/open_threats

Final selection logic (deterministic):

  • If there is at least one A - Completely reliable source → reliability is A - Completely reliable
  • Else if there is at least one B - Usually reliable source → reliability is B - Usually reliable
  • Else → reliability is None

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.

Available Commands

The following commands are available in this integration:

Note: Commands now use the gibti- prefix. Legacy gibtia- commands remain available for backward compatibility and are marked as deprecated in the integration settings.

  • gibti-get-available-collections - Returns list of available collections
  • gibti-get-compromised-account-info - Performs Group-IB event lookup in compromised/account collection
  • gibti-get-compromised-card-group-info - Performs Group-IB event lookup in compromised/card collection
  • gibti-get-compromised-masked-card-info - Performs Group-IB event lookup in compromised/masked_card collection
  • gibti-get-compromised-breached-info - Performs Group-IB event lookup in compromised/breached collection
  • gibti-get-phishing-group-info - Performs Group-IB event lookup in attacks/phishing_group collection
  • gibti-get-phishing-kit-info - Performs Group-IB event lookup in attacks/phishing_kit collection
  • gibti-get-osi-git-leak-info - Performs Group-IB event lookup in osi/git_repository collection
  • gibti-get-osi-public-leak-info - Performs Group-IB event lookup in osi/public_leak collection
  • gibti-get-osi-vulnerability-info - Performs Group-IB event lookup in osi/vulnerability collection
  • gibti-get-malware-malware-info - Performs Group-IB event lookup in malware/malware collection
  • gibti-get-compromised-mule-info - Performs Group-IB event lookup in compromised/mule collection
  • gibti-get-compromised-spd-info - Performs Group-IB event lookup in compromised/spd (suspicious payment details) collection
  • gibti-get-attacks-ddos-info - Performs Group-IB event lookup in attacks/ddos collection
  • gibti-get-attacks-deface-info - Performs Group-IB event lookup in attacks/deface collection
  • gibti-get-threat-info - Performs Group-IB event lookup in hi/threat or apt/threat collection
  • gibti-get-threat-actor-info - Performs Group-IB event lookup in hi/threat_actor or apt/threat_actor collection
  • gibti-get-suspicious-ip-tor-node-info - Performs Group-IB event lookup in suspicious_ip/tor_node collection
  • gibti-get-suspicious-ip-open-proxy-info - Performs Group-IB event lookup in suspicious_ip/open_proxy collection
  • gibti-get-suspicious-ip-socks-proxy-info - Performs Group-IB event lookup in suspicious_ip/socks_proxy collection
  • gibti-get-suspicious-ip-vpn-info - Performs Group-IB event lookup in suspicious_ip/vpn collection
  • gibti-get-suspicious-ip-scanner-info - Performs Group-IB event lookup in suspicious_ip/scanner collection
  • gibti-get-malware-cnc-info - Performs Group-IB event lookup in malware/cnc collection
  • gibti-global-search - Performs global Group-IB search across all collections
  • gibtia-local-search - Performs Group-IB search in selected collection

gibti-get-compromised-account-info


Command performs Group-IB event lookup in compromised/account collection with provided ID.

Base Command

gibti-get-compromised-account-info

Input

Argument Name Description Required
id GIB event id.
e.g.: 253b9a136f0d574149fc43691eaf7ae27aff141a.
Required

Context Output

Path Type Description
GIBTIA.CompromisedAccount.client.ipv4.asn String Victim IP address
GIBTIA.CompromisedAccount.client.ipv4.countryName String Country name
GIBTIA.CompromisedAccount.client.ipv4.ip String Victim IP address
GIBTIA.CompromisedAccount.client.ipv4.region String Region name
GIBTIA.CompromisedAccount.cnc.domain String Event CNC domain
GIBTIA.CompromisedAccount.cnc.url String CNC URL
GIBTIA.CompromisedAccount.cnc.ipv4.ip String CNC IP address
GIBTIA.CompromisedAccount.dateCompromised Date Date of compromise
GIBTIA.CompromisedAccount.dateDetected Date Date of detection
GIBTIA.CompromisedAccount.dropEmail.email String Email where compromised data were sent to
GIBTIA.CompromisedAccount.dropEmail.domain String Email domain
GIBTIA.CompromisedAccount.login String Compromised login
GIBTIA.CompromisedAccount.password String Compromised password
GIBTIA.CompromisedAccount.malware.name String Malware name
GIBTIA.CompromisedAccount.malware.id String Group-IB malware ID
GIBTIA.CompromisedAccount.person.name String Card owner name
GIBTIA.CompromisedAccount.person.email String Card owner e-mail
GIBTIA.CompromisedAccount.portalLink String Link to GIB incident
GIBTIA.CompromisedAccount.threatActor.name String Associated threat actor
GIBTIA.CompromisedAccount.threatActor.isAPT Boolean Is threat actor APT group
GIBTIA.CompromisedAccount.threatActor.id String Threat actor GIB ID
GIBTIA.CompromisedAccount.id String Group-IB incident ID
GIBTIA.CompromisedAccount.evaluation.severity String Event severity

Command Example

!gibti-get-compromised-account-info id=253b9a136f0d574149fc43691eaf7ae27aff141a

Human Readable Output

Feed from compromised/account with ID 253b9a136f0d574149fc43691eaf7ae27aff141a

client ipv4 ip cnc cnc cnc domain cnc ipv4 asn cnc ipv4 city cnc ipv4 countryCode cnc ipv4 countryName cnc ipv4 ip cnc ipv4 provider cnc ipv4 region cnc url companyId dateDetected domain evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp evaluation ttl id login malware id malware name malware stixGuid oldId password portalLink silentInsert sourceType stixGuid
0.0.0.0 ««««http://some.com»»»» some.com AS1111 City RU Country 11.11.11.11 some.com City http://some.com -1 2020-02-22T01:21:03+00:00 some.com A2 80 100 red red 90 253b9a136f0d574149fc43691eaf7ae27aff141a some.com 411ac9df6c5515922a56e30013e8b8b366eeec80 PredatorStealer 2f7650f4-bc72-2068-d1a5-467b688975d8 396792583 @some@ https://group-ib.com/cd/accounts?searchValue=id:253b9a136f0d574149fc43691eaf7ae27aff141a 0 Botnet 8abb3aa9-e351-f837-d61a-856901c3dc9d

URL indicator

gibid severity value
253b9a136f0d574149fc43691eaf7ae27aff141a red http://some.com

Domain indicator

gibid severity value
253b9a136f0d574149fc43691eaf7ae27aff141a red some.com

IP indicator

asn geocountry geolocation gibid severity value
AS1111 Country City 253b9a136f0d574149fc43691eaf7ae27aff141a red 11.11.11.11

gibti-get-compromised-breached-info


Command performs Group-IB event lookup in compromised/breached collection with provided ID.

Base Command

gibti-get-compromised-breached-info

Input

Argument Name Description Required
id GIB event id.
e.g.: 6fd344f340f4bdc08548cb36ded62bdf.
Required

Context Output

Path Type Description
GIBTIA.DataBreach.email String List of breached emails
GIBTIA.DataBreach.leakName String Name of the leak
GIBTIA.DataBreach.password String List of breached passwords
GIBTIA.DataBreach.uploadTime Date Date of breached data upload
GIBTIA.DataBreach.id String Group-IB incident ID
GIBTIA.DataBreach.evaluation.severity String Event severity

Command Example

!gibti-get-compromised-breached-info id=277c4112d348c91f6dabe9467f0d18ba

Human Readable Output

Feed from compromised/breached with ID 277c4112d348c91f6dabe9467f0d18ba

addInfo email evaluation id leakName password uploadTime
address:
some@gmail.com admiraltyCode: C3
credibility: 50
reliability: 50
severity: green
tlp: amber
ttl: null
277c4112d348c91f6dabe9467f0d18ba some.com AC91C480FDE9D7ACB8AC4B78310EB2TD,
1390DDDFA28AE085D23518A035703112
2021-06-12T03:02:00

gibti-get-compromised-mule-info


Command performs Group-IB event lookup in compromised/mule collection with provided ID.

Base Command

gibti-get-compromised-mule-info

Input

Argument Name Description Required
id GIB event id.
e.g.: 50a3b4abbfca5dcbec9c8b3a110598f61ba93r33.
Required

Context Output

Path Type Description
GIBTIA.CompromisedMule.account String Account number (card/phone), which was used by threat actor to cash out
GIBTIA.CompromisedMule.cnc.ipv4.asn String CNC ASN
GIBTIA.CompromisedMule.cnc.ipv4.countryName String Country name
GIBTIA.CompromisedMule.cnc.ipv4.ip String Victim IP address
GIBTIA.CompromisedMule.cnc.ipv4.region String Region name
GIBTIA.CompromisedMule.cnc.url String CNC URL
GIBTIA.CompromisedMule.cnc.domain String CNC domain
GIBTIA.CompromisedMule.dateAdd Date Date of detection
GIBTIA.CompromisedMule.malware.name String Malware name
GIBTIA.CompromisedMule.portalLink String Link to GIB incident
GIBTIA.CompromisedMule.threatActor.name String Associated threat actor
GIBTIA.CompromisedMule.threatActor.id String Threat actor GIB ID
GIBTIA.CompromisedMule.threatActor.isAPT Boolean Is threat actor APT group
GIBTIA.CompromisedMule.id String Group-IB incident ID
GIBTIA.CompromisedMule.sourceType String Information source
GIBTIA.CompromisedMule.evaluation.severity String Event severity

Command Example

!gibti-get-compromised-mule-info id=50a3b4abbfca5dcbec9c8b3a110598f61ba90a99

Human Readable Output

Feed from compromised/mule with ID 50a3b4abbfca5dcbec9c8b3a110598f61ba90a99

account cnc cnc cnc domain cnc ipv4 ip cnc url dateAdd evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp evaluation ttl hash id malware id malware name malware stixGuid oldId organization name portalLink sourceType stixGuid type
1111111111111111 ««««««««««««««««http://some.com»»»»»»»»»»»»»»»» some 11.11.11.11 http://some.com 2020-02-21T13:02:00+00:00 A2 80 100 red amber 30 some 50a3b4abbfca5dcbec9c8b3a110598f61ba90a99 5a2b741f8593f88178623848573abc899f9157d4 Anubis 7d837524-7b01-ddc9-a357-46e7136a9852 392993084 Some https://group-ib.com/cd/mules?searchValue=id:50a3b4abbfca5dcbec9c8b3a110598f61ba90a99 Botnet 2da6b164-9a12-6db5-4346-2a80a4e03255 Person

URL indicator

gibid severity value
50a3b4abbfca5dcbec9c8b3a110598f61ba90a99 red http://some.com

Domain indicator

gibid severity value
50a3b4abbfca5dcbec9c8b3a110598f61ba90a99 red some

IP indicator

gibid severity value
50a3b4abbfca5dcbec9c8b3a110598f61ba90a99 red 11.11.11.11

gibti-get-compromised-spd-info


Command performs Group-IB event lookup in compromised/spd (suspicious payment details) collection with provided ID. Returns payment-related observables (e.g. cryptocurrency wallets) linked to threat actors and leaks, including type, value, events, sources, malware, and evaluation (severity, TLP, TTL).

Base Command

gibti-get-compromised-spd-info

Input

Argument Name Description Required
id GIB event id.
e.g.: 5120a3b4abbfca5dcbed3ac9c8b3a110598f61.
Required

Context Output

Path Type Description
GIBTIA.CompromisedSPD.id String Group-IB SPD incident ID.
GIBTIA.CompromisedSPD.type String Observable type (e.g. Cryptocurrency Wallet).
GIBTIA.CompromisedSPD.value.value String Main observable value (wallet address, etc.).
GIBTIA.CompromisedSPD.serviceType String Service type (e.g. BTCLike, XMRLike).
GIBTIA.CompromisedSPD.ownerName String Owner name if available.
GIBTIA.CompromisedSPD.illegalScore Number Illegal score.
GIBTIA.CompromisedSPD.portalLink String Link to GIB incident.
GIBTIA.CompromisedSPD.events Unknown Events table (compromisedAt, detectedAt, source, malware, threatActor).
GIBTIA.CompromisedSPD.sources Unknown Sources table (name, type).
GIBTIA.CompromisedSPD.malware Unknown Malware table (id, name, stixGuid).
GIBTIA.CompromisedSPD.threatActor Unknown Threat actor table (id, name, stixGuid).
GIBTIA.CompromisedSPD.evaluation.severity String Event severity.
GIBTIA.CompromisedSPD.evaluation.tlp String TLP.
GIBTIA.CompromisedSPD.evaluation.ttl Number TTL (days).

Command Example

!gibti-get-compromised-spd-info id=5120a3b4abbfca5dcbed3ac9c8b3a110598f61

Human Readable Output

Feed from compromised/spd with ID 5120a3b4abbfca5dcbed3ac9c8b3a110598f61

id type value serviceType illegalScore portalLink evaluation severity evaluation tlp evaluation ttl
5120a3b4abbfca5dcbed3ac9c8b3a110598f61 Cryptocurrency Wallet bc1qrc4zze8zr96pwt49fn6nq53rks625guzn7navy BTCLike 100 https://tap.group-ib.com/cd/suspicious-payment-details?id=5120a3b4abbfca5dcbed3ac9c8b3a110598f61 red amber 30

Events, sources, malware, and threat actor tables are included in the full feed object.

gibti-get-osi-git-leak-info


Command performs Group-IB event lookup in osi/git_leak collection with provided ID.

Base Command

gibti-get-osi-git-leak-info

Input

Argument Name Description Required
id GIB event id.
e.g.: f201c253ac71f7d78db39fa111a2af9d7ee7a3f7.
Required

Context Output

Path Type Description
GIBTIA.GitLeak.dateDetected Date Leak detection date
GIBTIA.GitLeak.matchesType String List of matches type
GIBTIA.GitLeak.name String GIT filename
GIBTIA.GitLeak.repository String GIT repository
GIBTIA.GitLeak.revisions.file String Leaked file link
GIBTIA.GitLeak.revisions.fileDiff String Leaked file diff
GIBTIA.GitLeak.revisions.info.authorName String Revision author
GIBTIA.GitLeak.revisions.info.authorEmail String Author name
GIBTIA.GitLeak.revisions.info.dateCreated Date Revision creation date
GIBTIA.GitLeak.source String Source(github/gitlab/etc.)
GIBTIA.GitLeak.evaluation.severity String Event severity

Command Example

!gibti-get-osi-git-leak-info id=ead0d8ae9f2347789941ebacde88ad2e3b1ef691

Human Readable Output

Feed from osi/git_leak with ID ead0d8ae9f2347789941ebacde88ad2e3b1ef691

companyId dateDetected dateUpdated evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp evaluation ttl file fileId id matchesType matchesTypeCount card matchesTypeCount cisco matchesTypeCount commonKeywords matchesTypeCount domain matchesTypeCount dsn matchesTypeCount email matchesTypeCount google matchesTypeCount ip matchesTypeCount keyword matchesTypeCount login matchesTypeCount metasploit matchesTypeCount nmap matchesTypeCount pgp matchesTypeCount sha matchesTypeCount slackAPI matchesTypeCount ssh name repository source
40,
1872,
2060,
2248,
2522,
2692
2020-03-12T01:12:00+00:00 2020-02-11T01:12:00+00:00 A6 100 100 green amber 30 https://group-ib.com/api/v2/osi/git_leak/ead0d8ae9f2347789941ebacde88ad2e3b1ef691/file/bWFpbi0zOTFkYjVkNWYxN2FiNmNiYmJmN2MzNWQxZjRkMDc2Y2I0YzgzMGYwOTdiMmE5ZWRkZDJkZjdiMDY1MDcwOWE3 391db5d5f17ab6cbbbf7c35d1f4d076cb4c830f097b2a9eddd2df7b0650709a7 ead0d8ae9f2347789941ebacde88ad2e3b1ef691 commonKeywords,
keyword
0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0 some some.com github

revisions table

bind companyId data file fileDiff fileDiffId fileId hash info parentFileId
{‘bindBy’: ‘cert’, ‘companyId’: [2692], ‘data’: ‘cert’, ‘type’: ‘keyword’} 2692 commonKeywords: {“password”: [“password”]} https://group-ib.com/api/v2/osi/git_leak/ead0d8ae9f2347789941ebacde88ad2e3b1ef691/file/cmV2aXNpb24tZmlsZS0zOTFkYjVkNWYxN2FiNmNiYmJmN2MzNWQxZjRkMDc2Y2I0YzgzMGYwOTdiMmE5ZWRkZDJkZjdiMDY1MDcwOWE3 https://group-ib.com/api/v2/osi/git_leak/ead0d8ae9f2347789941ebacde88ad2e3b1ef691/file/cmV2aXNpb24tZmlsZURpZmYtMzkxZGI1ZDVmMTdhYjZjYmJiZjdjMzVkMWY0ZDA3NmNiNGM4MzBmMDk3YjJhOWVkZGQyZGY3YjA2NTA3MDlhNw== a2187ee179076a22e550e8f7fbc51840e87aba260431ab9cb2d4e0192ad4134c 391db5d5f17ab6cbbbf7c35d1f4d076cb4c830f097b2a9eddd2df7b0650709a7 Some authorEmail: some@gmail.com
authorName: some
dateCreated: 2020-01-03T11:17:52+00:00
timestamp: 1617794272
ead0d8ae9f2347789941ebacde88ad2e3b1ef691

gibti-get-osi-public-leak-info


Command performs Group-IB event lookup in osi/public_leak collection with provided ID.

Base Command

gibti-get-osi-public-leak-info

Input

Argument Name Description Required
id GIB event id.
e.g.: a9a5b5cb9b971a2a037e3a0a30654185ea148095.
Required

Context Output

Path Type Description
GIBTIA.PublicLeak.created Date Leak event detection date
GIBTIA.PublicLeak.data String Leaked data
GIBTIA.PublicLeak.hash String Leak data hash
GIBTIA.PublicLeak.linkList.author String Leak entry author
GIBTIA.PublicLeak.linkList.dateDetected Date Leak detection date
GIBTIA.PublicLeak.linkList.datePublished Date Leak publish date
GIBTIA.PublicLeak.linkList.hash String Leak hash
GIBTIA.PublicLeak.linkList.link String Leak link
GIBTIA.PublicLeak.linkList.source String Leak source
GIBTIA.PublicLeak.matches String Matches
GIBTIA.PublicLeak.portalLink String Group-IB portal link
GIBTIA.PublicLeak.evaluation.severity String Event severity

Command Example

!gibti-get-osi-public-leak-info id=a09f2354e52d5fa0a8697c8df0b4ed99cc956273

Human Readable Output

Feed from osi/public_leak with ID a11f2354e52d5fa0a8697c8df0b4ed99cc956211

created data evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp evaluation ttl hash id language portalLink size updated useful
2020-02-02T13:52:01+03:00 Big chunk of data C3 50 50 green amber 30 a11f2354e52d5fa0a8697c8df0b4ed99cc956211 a11f2354e52d5fa0a8697c8df0b4ed99cc956211 java https://group-ib.com/osi/public_leak?searchValue=id:a09f2354e52d5fa0a8697c8df0b4ed99cc956273 709 B 2021-04-01T14:57:01+03:00 1

linkList table

dateDetected datePublished hash itemSource link size source status
2021-04-01T14:57:01+03:00 2021-04-01T14:50:45+03:00 5d9657dbdf59487a6031820add2cacbe54e86814 api https://some.com 709 some.com 1

gibti-get-osi-vulnerability-info


Command performs Group-IB event lookup in osi/vulnerability collection with provided ID.

Base Command

gibti-get-osi-vulnerability-info

Input

Argument Name Description Required
id GIB event id.

e.g.: CVE-2021-27152.
Required

Context Output

Path Type Description
GIBTIA.OSIVulnerability.affectedSoftware.name String Affected software name
GIBTIA.OSIVulnerability.affectedSoftware.operator String Affected software version operator( ex. le=less or equal)
GIBTIA.OSIVulnerability.affectedSoftware.version String Affected software version
GIBTIA.OSIVulnerability.bulletinFamily String Bulletin family
GIBTIA.OSIVulnerability.cvss.score String CVSS score
GIBTIA.OSIVulnerability.cvss.vector String CVSS vector
GIBTIA.OSIVulnerability.dateLastSeen Date Date last seen
GIBTIA.OSIVulnerability.datePublished Date Date published
GIBTIA.OSIVulnerability.description String Vulnerability description
GIBTIA.OSIVulnerability.id String Vulnerability ID
GIBTIA.OSIVulnerability.reporter String Vulnerability reporter
GIBTIA.OSIVulnerability.title String Vulnerability title
GIBTIA.OSIVulnerability.evaluation.severity String Event severity

Command Example

!gibti-get-osi-vulnerability-info id=CVE-2021-27152

Human Readable Output

Feed from osi/vulnerability with ID CVE-2021-27152

bulletinFamily cvss score cvss vector dateLastSeen dateModified datePublished description displayOptions isFavourite displayOptions isHidden evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp evaluation ttl exploitCount extCvss base extCvss environmental extCvss exploitability extCvss impact extCvss mImpact extCvss overall extCvss temporal extCvss vector extDescription href id lastseen modified portalLink provider published references reporter title type
NVD 7.5 AV:N/AC:L/Au:N/C:P/I:P/A:P 2021-02-11T14:35:24+03:00 2021-02-11T00:45:00+03:00 2021-02-10T19:15:00+03:00 Description false false A1 100 100 red green 30 0 9.8 0.0 3.9 5.9 0.0 9.8 0.0 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H Big description ««««https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-27152»»»» CVE-2021-27152 2021-02-11T14:35:24+03:00 2021-02-11T00:45:00+03:00 https://group-ib.com/osi/vulnerabilities?searchValue=id:CVE-2021-27152 some.com 2021-02-10T19:15:00+03:00 https://pierrekim.github.io/blog/2021-01-12-fiberhome-ont-0day-vulnerabilities.html#httpd-hardcoded-credentials,
https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2021-27152
some.com CVE-2021-27152 cve

softwareMixed table

os osVendor osVersion vendor
some_firmware some some some

gibti-get-attacks-ddos-info


Command performs Group-IB event lookup in attacks/ddos collection with provided ID.

Base Command

gibti-get-attacks-ddos-info

Input

Argument Name Description Required
id GIB event id.
e.g.: 26a05baa4025edff367b058b13c6b43e820538a5.
Required

Context Output

Path Type Description
GIBTIA.AttacksDDoS.cnc.url String CNC URL
GIBTIA.AttacksDDoS.cnc.domain String CNC domain
GIBTIA.AttacksDDoS.cnc.ipv4.asn String CNC ASN
GIBTIA.AttacksDDoS.cnc.ipv4.countryName String CNC IP country name
GIBTIA.AttacksDDoS.cnc.ipv4.ip String CNC IP address
GIBTIA.AttacksDDoS.cnc.ipv4.region String CNC region name
GIBTIA.AttacksDDoS.target.ipv4.asn String DDoS target ASN
GIBTIA.AttacksDDoS.target.ipv4.countryName String DDoS target country name
GIBTIA.AttacksDDoS.target.ipv4.ip String DDoS target IP address
GIBTIA.AttacksDDoS.target.ipv4.region String DDoS target region name
GIBTIA.AttacksDDoS.target.category String DDoS target category
GIBTIA.AttacksDDoS.target.domain String DDoS target domain
GIBTIA.AttacksDDoS.threatActor.id String Associated threat actor ID
GIBTIA.AttacksDDoS.threatActor.name String Associated threat actor
GIBTIA.AttacksDdos.threatActor.isAPT Boolean Is threat actor APT
GIBTIA.AttacksDDoS.id String GIB incident ID
GIBTIA.AttacksDDoS.evaluation.severity String Event severity

Command Example

!gibti-get-attacks-ddos-info id=26a05baa4025edff367b058b13c6b43e820538a5

Human Readable Output

Feed from attacks/ddos with ID 26a05baa4025edff367b058b13c6b43e820538a5

cnc cnc cnc domain cnc ipv4 asn cnc ipv4 city cnc ipv4 countryCode cnc ipv4 countryName cnc ipv4 ip cnc ipv4 provider cnc ipv4 region companyId dateBegin dateEnd dateReg evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp evaluation ttl id oldId portalLink protocol source stixGuid target domainsCount target ipv4 asn target ipv4 city target ipv4 countryCode target ipv4 countryName target ipv4 ip target ipv4 provider target ipv4 region target port type
some.com some.com AS11111 Some US United States 11.11.11.11 Some Some -1 2021-01-16T02:58:53+00:00 2021-01-16T02:58:55+00:00 2021-01-16 A2 90 90 red green 30 26a05baa4025edff367b058b13c6b43e820538a5 394657345 https://group-ib.com/attacks/ddos?searchValue=id:26a05baa4025edff367b058b13c6b43e820538a5 udp honeypot_logs:1 ea05c117-2cca-b3cd-f033-a8e16e5db3c2 0 AS11111 Some US United States 11.11.11.11 Some Some 55843 DNS Reflection

Domain indicator

gibid severity value
26a05baa4025edff367b058b13c6b43e820538a5 red some.com

IP indicator

asn geocountry geolocation gibid severity value
AS11111 United States Some 26a05baa4025edff367b058b13c6b43e820538a5 red 11.11.11.11

gibti-get-attacks-deface-info


Command performs Group-IB event lookup in attacks/deface collection with provided ID.

Base Command

gibti-get-attacks-deface-info

Input

Argument Name Description Required
id GIB event id.
e.g.: 6009637a1135cd001ef46e21.
Required

Context Output

Path Type Description
GIBTIA.AttacksDeface.date Date Date of deface
GIBTIA.AttacksDeface.id String GIB incident ID
GIBTIA.AttacksDeface.targetIp.asn String Victim ASN
GIBTIA.AttacksDeface.targetIp.countryName String Victim country name
GIBTIA.AttacksDeface.targetIp.region String Victim IP region name
GIBTIA.AttacksDeface.threatActor.id String Associated threat actor ID
GIBTIA.AttacksDeface.threatActor.name String Associated threat actor
GIBTIA.AttacksDeface.threatActor.isAPT Boolean Is threat actor APT
GIBTIA.AttacksDeface.url String URL of compromised resource
GIBTIA.AttacksDeface.evaluation.severity String Event severity

Command Example

!gibti-get-attacks-deface-info id=6009637a1135cd001ef46e21

Human Readable Output

Feed from attacks/deface with ID 6009637a1135cd001ef46e21

date evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp evaluation ttl id mirrorLink portalLink providerDomain siteUrl source targetDomain targetIp countryName targetIp ip threatActor id threatActor isAPT threatActor name tsCreate url
2021-01-21T02:22:18+00:00 B2 80 80 orange amber 30 6009637a1135cd001ef46e21 https://some.com/id:-6009637a1135cd001ef46e21: https://group-ib.com/attacks/deface?searchValue=id:6009637a1135cd001ef46e21 some.com ««««««««««««««««http://some.com»»»»»»»»»»»»»»»» some.com some.com Indonesia 11.11.11.11 d7ff75c35f93dce6f5410bba9a6c206bdff66555 false FRK48 2021-01-21T11:19:52+00:00 http://some.com

URL indicator

gibid severity value
6009637a1135cd001ef46e21 orange http://some.com

Domain indicator

gibid severity value
6009637a1135cd001ef46e21 orange some.com

IP indicator

geocountry gibid severity value
Indonesia 6009637a1135cd001ef46e21 orange 11.11.11.11

gibti-get-phishing-kit-info


Command performs Group-IB event lookup in attacks/phishing_kit collection with provided ID.

Base Command

gibti-get-phishing-kit-info

Legacy alias gibtia-get-phishing-kit-info remains available for backward compatibility.

Input

Argument Name Description Required
id GIB event id. Required

Command Example

!gibti-get-phishing-kit-info id=<phishing-kit-id>

gibti-get-threat-info


Command performs Group-IB event lookup in hi/threat (or in apt/threat if the APT flag is true) collection with provided ID.

Base Command

gibti-get-threat-info

Input

Argument Name Description Required
id GIB event id.
e.g.: 1b09d389d016121afbffe481a14b30ea995876e4.
Required
isAPT Is threat APT. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
GIBTIA.Threat.contacts.account String Threat accounts found in this threat action.
GIBTIA.Threat.contacts.flag String Is account fake or not
GIBTIA.Threat.contacts.service String Account service
GIBTIA.Threat.contacts.type String Type of account(social_network/email/wallet etc.)
GIBTIA.Threat.countries String Affected countries
GIBTIA.Threat.createdAt Date Threat report creation date
GIBTIA.Threat.cveList.name String List of abused CVE
GIBTIA.Threat.dateFirstSeen Date Attack first seen date
GIBTIA.Threat.dateLastSeen Date Attack last seen date
GIBTIA.Threat.datePublished Date Date published
GIBTIA.Threat.description String Threat description
GIBTIA.Threat.forumsAccounts.url String Related forum URL
GIBTIA.Threat.forumsAccounts.nickname String Related forums account
GIBTIA.Threat.forumsAccounts.registeredAt Date Related forums account registration date
GIBTIA.Threat.forumsAccounts.messageCount Number Related forums messages count
GIBTIA.Threat.id String GIB internal threat ID
GIBTIA.Threat.indicators String Can be either network or file indicators
GIBTIA.Threat.langs String Languages actors related
GIBTIA.Threat.malwareList.name String Related Malware Name
GIBTIA.Threat.malwareList.id String Related malware GIB internal ID
GIBTIA.Threat.mitreMatrix.attackPatternId String MITRE attack pattern ID
GIBTIA.Threat.mitreMatrix.attackTactic String MITRE attack tactic name
GIBTIA.Threat.mitreMatrix.attackType String MITRE attack type
GIBTIA.Threat.mitreMatrix.id String MITRE attack id
GIBTIA.Threat.regions String Regions affected by attack
GIBTIA.Threat.reportNumber String GIB report number
GIBTIA.Threat.sectors String Affected sectors
GIBTIA.Threat.shortDescription String Short description
GIBTIA.Threat.title String Threat title
GIBTIA.Threat.targetedCompany String Targeted company name
GIBTIA.Threat.ThreatActor.name String Threat actor name
GIBTIA.Threat.ThreatActor.id String Threat actor ID
GIBTIA.Threat.ThreatActor.isAPT Boolean Is threat actor APT group
GIBTIA.Threat.sources String Sources links
GIBTIA.Threat.evaluation.severity String Event severity

Command Example

!gibti-get-threat-info id=1b09d389d016121afbffe481a14b30ea995876e4 isAPT=true

Human Readable Output

Feed from threat with ID 1b09d389d016121afbffe481a14b30ea995876e4

createdAt dateFirstSeen dateLastSeen datePublished deleted description evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp id isPublished isTailored langs oldId reportNumber sectors threatActor country threatActor id threatActor isAPT threatActor name title type updatedAt
2021-01-15T16:53:20+03:00 2021-01-15 2021-01-15 2021-01-15 false Big description B1 100 80 orange amber 1b09d389d016121afbffe481a14b30ea995876e4 true false en,
com
4c01c2d4-5ebb-44d8-9e91-be89231b0eb3 CP-2501-1653 financial-services,
finance
KP 5e9f20fdcf5876b5772b3d09b432f4080711ac5f true Lazarus Lazarus launches new attack with cryptocurrency trading platforms threat 2021-04-02T14:08:03+03:00

files table

hash mime name size
fa5b6b2f074ba6eb58f8b093f0e92cb8ff44b655dc8e9ce93f850e71474e4e11 image/png fa5b6b2f074ba6eb58f8b093f0e92cb8ff44b655dc8e9ce93f850e71474e4e11 284731
a6851a6b91759d00afce8e65c0e5087429812b8c49d39631793d8b6bdeb08711 image/png a6851a6b91759d00afce8e65c0e5087429812b8c49d39631793d8b6bdeb08711 129240
644f5b8e38f55b82f811240af7c4abdaf8c8bc18b359f8f169074ba881d93b1d image/png 644f5b8e38f55b82f811240af7c4abdaf8c8bc18b359f8f169074ba881d93b1d 556552
623102f6cf9d2e6c978898117b7b5b85035b3d5e67c4ee266879868c9eb24dd2 image/png 623102f6cf9d2e6c978898117b7b5b85035b3d5e67c4ee266879868c9eb24dd2 209254

mitreMatrix table

attackPatternId attackTactic attackType id params
attack-pattern–45242287-2964-4a3e-9373-159fad4d8195 establish-&-maintain-infrastructure pre_attack_tactics PRE-T1105 data:

indicatorRelationships table

sourceId targetId
9f3a2a244570a38e772a35d7c9171eed92bec6f7 12cad1ca535a92a2ed306c0edf3025e7d9776693

indicators table

deleted id langs params seqUpdate type
false 9f3a2a244570a38e772a35d7c9171eed12bec6f7 en hashes: {“md4”: “”, “md5”: “8397ea747d2ab50da4f876a36d631272”, “md6”: “”, “ripemd160”: “”, “sha1”: “48a6d5141e25b6c63ad8da20b954b56afe512031”, “sha224”: “”, “sha256”: “89b5e248c222ebf2cb3b525d3650259e01cf7d8fff5e1aa15ccd7512b1e63957”, “sha384”: “”, “sha512”: “”, “whirlpool”: “”}
name: some.com
size: null
16107188499162 file
false 8b96c56cbc980c1e3362060ffa953e65281fb1df en domain: some.com
ipv4:
ipv6:
ssl:
url: https://some.com
16107188498393 network
false 42a9929807fd954918f9bb603135754be7a6e11c en hashes: {“md4”: “”, “md5”: “5d43baf1c9e9e3a939e5defd8f3fbd1d”, “md6”: “”, “ripemd120”: “”, “sha1”: “d5ff73c043f3bb75dd749636307500b60a336150”, “sha224”: “”, “sha256”: “867c8b49d29ae1f6e4a7cd31b6fe7e278753a1ba03d4be338ed11fd1efc3dd12”, “sha384”: “”, “sha512”: “”, “whirlpool”: “”}
name: 5d43baf1c9e9e3a939e5defd8f8fbd1d
size: null
16107188498634 file
false 12cad1ca535a92a2ed306c0edf3025e7d9776612 en domain: some.com
ipv4:
ipv6:
ssl:
url: https://some.com
16107188498908 network

gibti-get-threat-actor-info


Command performs Group-IB event lookup in hi/threat_actor (or in apt/threat_actor if the APT flag is true) collection with provided ID.

Base Command

gibti-get-threat-actor-info

Input

Argument Name Description Required
id GIB internal threatActor ID.
e.g.: 0d4496592ac3a0f5511cd62ef29887f48d9cb545.
Required
isAPT Is threat actor APT group. Possible values are: true, false. Default is false. Optional

Context Output

Path Type Description
GIBTIA.ThreatActor.aliases String Threat actor aliases
GIBTIA.ThreatActor.country String Threat actor country
GIBTIA.ThreatActor.createdAt Date Threat actor record creation time
GIBTIA.ThreatActor.description String Threat actor description
GIBTIA.ThreatActor.goals String Threat actor goals sectors(financial, diplomatic, etc.)
GIBTIA.ThreatActor.id String Threat actor id
GIBTIA.ThreatActor.isAPT Boolean Threat actor is APT
GIBTIA.ThreatActor.labels String GIB internal threat actor labels(hacker, nation-state, etc.)
GIBTIA.ThreatActor.langs String Threat actor communication language
GIBTIA.ThreatActor.name String Threat actor name
GIBTIA.ThreatActor.roles String Threat actor roles
GIBTIA.ThreatActor.stat.countries String Threat actor countries activity found in
GIBTIA.ThreatActor.stat.dateFirstSeen Date Date first seen
GIBTIA.ThreatActor.stat.dateLastSeen Date Date last seen
GIBTIA.ThreatActor.stat.regions String Threat actor activity regions
GIBTIA.ThreatActor.stat.reports.datePublished Date Related threat report publishing date
GIBTIA.ThreatActor.stat.reports.id String Related threat report id
GIBTIA.ThreatActor.stat.reports.name.en String Related threat report language
GIBTIA.ThreatActor.stat.sectors String Sectors attacked by threat actor

Command Example

!gibti-get-threat-actor-info id=0d4496592ac3a0f5511cd62ef29887f48d9cb545 isAPT=true

Human Readable Output

Feed from threat_actor with ID 0d4496592ac3a0f5511cd62ef29887f48d9cb545

aliases country createdAt deleted description goals id isAPT isPublished labels langs name roles spokenOnLangs stat countries stat dateFirstSeen stat dateLastSeen stat regions stat sectors stixGuid updatedAt
SectorC08 RU 2018-09-26T16:59:50+03:00 false Big description Information 0d4496592ac3a0f5511cd62ef29887f48d9cb545 true true spy en Gamaredon agent com US 2013-06-01 2021-03-19 asia non-profit 63d0e4d4-9f55-4fa2-87af-b6c91ded80e0 2021-04-08T22:09:07+03:00

stat reports table

datePublished id name
2021-02-04 59dec5947c5adac898445e3958b1d05e1c260459 en: Template injection attacks from the Gamaredon group continued: protocol topics

gibti-get-suspicious-ip-tor-node-info


Command performs Group-IB event lookup in suspicious_ip/tor_node collection with provided ID.

Base Command

gibti-get-suspicious-ip-tor-node-info

Input

Argument Name Description Required
id GIB event id.
e.g.: 109.70.100.46.
Required

Context Output

Path Type Description
GIBTIA.SuspiciousIPTorNode.ipv4.asn String Tor node ASN
GIBTIA.SuspiciousIPTorNode.ipv4.countryName String Tor node IP country name
GIBTIA.SuspiciousIPTorNode.ipv4.ip String Tor node IP address
GIBTIA.SuspiciousIPTorNode.ipv4.region String Tor node IP region name
GIBTIA.SuspiciousIPTorNode.id String GIB id
GIBTIA.SuspiciousIPTorNode.evaluation.severity String Event severity

Command Example

!gibti-get-suspicious-ip-tor-node-info id=109.70.100.46

Human Readable Output

Feed from suspicious_ip/tor_node with ID 11.11.11.11

dateFirstSeen dateLastSeen evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp evaluation ttl id ipv4 ip portalLink source
2020-09-03T14:15:25+00:00 2021-04-25T03:15:29+00:00 A1 90 90 green green 30 11.11.11.11 11.11.11.11 https://group-ib.com/suspicious/tor?searchValue=id:11.11.11.11 some.com

IP indicator

gibid severity value
11.11.11.11 green 11.11.11.11

gibti-get-suspicious-ip-open-proxy-info


Command performs Group-IB event lookup in suspicious_ip/open_proxy collection with provided ID.

Base Command

gibti-get-suspicious-ip-open-proxy-info

Input

Argument Name Description Required
id GIB event id.
e.g.: cc6a2856da2806b03839f81aa214f22dbcfd7369.
Required

Context Output

Path Type Description
GIBTIA.SuspiciousIPOpenProxy.ipv4.asn String Proxy ASN
GIBTIA.SuspiciousIPOpenProxy.ipv4.countryName String Proxy IP country name
GIBTIA.SuspiciousIPOpenProxy.ipv4.ip String Proxy IP address
GIBTIA.SuspiciousIPOpenProxy.ipv4.region String Proxy IP region name
GIBTIA.SuspiciousIPOpenProxy.ipv4.port Number Proxy port
GIBTIA.SuspiciousIPOpenProxy.ipv4.source String Information source
GIBTIA.SuspiciousIPOpenProxy.ipv4.anonymous String Proxy anonymous level
GIBTIA.SuspiciousIPOpenProxy.id String GIB event ID
GIBTIA.SuspiciousIPOpenProxy.evaluation.severity String Event severity

Command Example

!gibti-get-suspicious-ip-open-proxy-info id=cc6a2856da2806b03839f81aa214f22dbcfd7369

Human Readable Output

Feed from suspicious_ip/open_proxy with ID cc6a2856da2806b03839f81aa214f22dbcfd7369

anonymous dateDetected dateFirstSeen evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp evaluation ttl id ipv4 countryCode ipv4 countryName ipv4 ip ipv4 provider oldId port portalLink source stixGuid type
11.11.11.11 2021-01-21T11:01:02+00:00 2020-03-19T23:01:01+00:00 C3 50 50 green white 15 cc6a2856da2806b03839f81aa214f22dbcfd7369 Country Code Country 11.11.11.11 Some 241549215 80 https://group-ib.com/suspicious/proxies?searchValue=id:cc6a2856da2806b03839f81aa214f22dbcfd7369 some.com c30604ac-94d5-b514-f1d1-7230ec13c739 http

IP indicator

geocountry gibid gibproxyanonymous gibproxyport severity source value
Country cc6a2856da2806b03839f81aa214f22dbcfd7369 11.11.11.11 80 green some.com 11.11.11.11

gibti-get-suspicious-ip-socks-proxy-info


Command performs Group-IB event lookup in suspicious_ip/socks_proxy collection with provided ID.

Base Command

gibti-get-suspicious-ip-socks-proxy-info

Input

Argument Name Description Required
id GIB event id.
e.g.: 02e385600dfc5bf9b3b3656df8e0e20f5fc5c86e.
Required

Context Output

Path Type Description
GIBTIA.SuspiciousIPSocksProxy.ipv4.asn String Proxy IP ASN
GIBTIA.SuspiciousIPSocksProxy.ipv4.countryName String Proxy IP country name
GIBTIA.SuspiciousIPSocksProxy.ipv4.ip String Proxy IP address
GIBTIA.SuspiciousIPSocksProxy.ipv4.region String Proxy IP region name
GIBTIA.SuspiciousIPSocksProxy.id String GIB ID
GIBTIA.SuspiciousIPSocksProxy.evaluation.severity String Event severity

Command Example

!gibti-get-suspicious-ip-socks-proxy-info id=02e385600dfc5bf9b3b3656df8e0e20f5fc5c86e

Human Readable Output

Feed from suspicious_ip/socks_proxy with ID 02e385600dfc5bf9b3b3656df8e0e20f5fc5c86e

dateDetected dateFirstSeen dateLastSeen evaluation admiraltyCode evaluation credibility evaluation reliability evaluation severity evaluation tlp evaluation ttl id ipv4 asn ipv4 countryCode ipv4 countryName ipv4 ip ipv4 provider oldId portalLink source stixGuid
2021-01-19T07:41:11+00:00 2021-01-19T07:41:11+00:00 2021-02-23T20:58:51+00:00 A1 100 90 green amber 2 02e385600dfc5bf9b3b3656df8e0e20f5fc5c86e AS11111 Country Code Country 11.11.11.11 Some 395880626 https://group-ib.com/suspicious/socks?searchValue=id:02e385600dfc5bf9b3b3656df8e0e20f5fc5c86e awmproxy.com 78cd5f78-e542-bf2c-fc40-e2a41b36dd97

IP indicator

asn geocountry gibid severity value
AS11111 Country 02e385600dfc5bf9b3b3656df8e0e20f5fc5c86e green 11.11.11.11

gibti-get-malware-cnc-info


Command performs Group-IB event lookup in malware/cnc collection by provided ID.

Base Command

gibti-get-malware-cnc-info

Input

Argument Name Description Required
id GIB event id.
e.g.: aeed277396e27e375d030a91533aa232444d0089.
Required

Context Output

Path Type Description
GIBTIA.MalwareCNC.dateDetected Date Date CNC detected
GIBTIA.MalwareCNC.dateLastSeen Date Date CNC last seen
GIBTIA.MalwareCNC.url String CNC URL
GIBTIA.MalwareCNC.domain String CNC domain
GIBTIA.MalwareCNC.ipv4.asn String CNC ASN
GIBTIA.MalwareCNC.ipv4.countryName String CNC IP country name
GIBTIA.MalwareCNC.ipv4.ip String CNC IP address
GIBTIA.MalwareCNC.ipv4.region String CNC region name
GIBTIA.MalwareCNC.malwareList.name String Associated malware
GIBTIA.MalwareCNC.threatActor.id String Associated threat actor ID
GIBTIA.MalwareCNC.threatActor.name String Associated threat actor
GIBTIA.MalwareCNC.threatActor.isAPT Boolean Is APT or not
GIBTIA.MalwareCNC.id String GIB event ID

Command Example

!gibti-get-malware-cnc-info id=aeed277396e27e375d030a91533aa232444d0089

Human Readable Output

Feed from malware/cnc with ID aeed277396e27e375d030a91533aa232444d0089

cnc dateDetected dateLastSeen domain id oldId stixGuid url
««««««««««««««««https://some.com»»»»»»»»»»»»»»»» 2021-04-25T13:37:23+00:00 2021-04-25T13:37:23+00:00 some.com aeed277396e27e375d030a91533aa232444d0089 211146923 417b2644-1105-d65b-4b67-a78e82f59b65 https://some.com

ipv4 table

asn countryCode countryName ip provider
AS1111 US United States 11.11.11.11 Some

malwareList table

id name stixGuid
e99c294ffe7b79655d6ef1f32add638d8a2d4b24 JS Sniffer - Poter 1ac5a303-ef6f-2d6a-ad20-a39196815a1a

URL indicator

gibid value
aeed277396e27e375d030a91533aa232444d0089 https://some.com

Domain indicator

gibid value
aeed277396e27e375d030a91533aa232444d0089 some.com

IP indicator

asn geocountry gibid value
AS1111 United States aeed277396e27e375d030a91533aa232444d0089 11.11.11.11

gibti-get-available-collections


Returns list of available collections.

Base Command

gibti-get-available-collections

Input

There are no input arguments for this command.

Context Output

Path Type Description
GIBTIA.OtherInfo.collections String List of availiable collections

Command Example

!gibti-get-available-collections

Human Readable Output

Available collections

collections
compromised/account,
compromised/card,
bp/phishing,
bp/phishing_kit,
osi/git_leak,
osi/public_leak,
malware/targeted_malware,
compromised/mule,
compromised/imei,
attacks/ddos,
attacks/deface,
attacks/phishing,
attacks/phishing_kit,
apt/threat,
hi/threat,
suspicious_ip/tor_node,
suspicious_ip/open_proxy,
suspicious_ip/socks_proxy,
malware/cnc,
osi/vulnerability,
hi/threat_actor,
apt/threat_actor

gibti-global-search


Command performs global Group-IB search

Base Command

gibti-global-search

Input

Argument Name Description Required
query Query you want to search.
e.g.: 8.8.8.8.
Required

Context Output

Path Type Description
apiPath String Name of collection in which found matches
count Number Count of feeds matching this query
GIBLink String Link to GIB TI&A interface

Command Example

!gibti-global-search query=100.100.100.100

Human Readable Output

Search results

apiPath count GIBLink
compromised/account 14  
attacks/phishing 1 https://group-ib.com/attacks/phishing?searchValue=100.100.100.100&q=100.100.100.100
bp/phishing 1  
osi/git_leak 5 https://group-ib.com/osi/git_leaks?searchValue=100.100.100.100&q=100.100.100.100
osi/public_leak 23 https://group-ib.com/osi/public_leak?searchValue=100.100.100.100&q=100.100.100.100

gibtia-local-search


Command performs Group-IB search in selected collection.

Base Command

gibtia-local-search

Input

Argument Name Description Required
collection_name Collection you want to search. Possible values are same as collection names in Data Collections Overview . Required
query Query you want to search.
e.g.: 8.8.8.8.
Required

Context Output

Path Type Description
GIBTI.search.local.id String Id of a feed that matches a query
GIBTI.search.local.additional_info String Additional info about feed
GIBTI.search.local.seqUpdate Number seqUpdate value of the page/portion that returned the feed
GIBTI.search.local.raw_feed String One-line JSON string of the full feed for War Room rendering (only when include_raw_feed=true)

Command Example

!gibtia-local-search collection_name=attacks/phishing query=100.100.100.100

Human Readable Output

Search results

id additional_info
8bd7e5cef2290b0c3f04bf283586406dceffe25d phishingDomain_domain: some.com

Configuration parameters

  • url — GIB TI URL (required)
  • credentials — Username (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • integration_reliability — Source Reliability (required)
  • disable_integration_reliability_override — Ignore Source Reliability override
  • enabled_reputation_commands — Enable reputation commands
  • isFetch — Fetch incidents
  • incident_collections — Collections to fetch
  • first_fetch — Incidents first fetch
  • exclude_combolist — Exclude All with Combolist type
  • combolist — Include combolist type in data
  • unique — Include unique type in data
  • enable_probable_corporate_access — Enable filter "Probable Corporate Access"
  • max_fetch — Number of requests per collection
  • skip_updated_incidents — Skip updated incidents (prevent duplicates)
  • dedup_lookback_days — Deduplication lookback (days)
  • limit — Limit (items per request)
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • hunting_rules — Hunting Rules

Commands (51)

  • domain

    Runs reputation on domains.

  • file

    Runs reputation on files.

  • gibti-get-attacks-ddos-info

    Command performs Group IB event lookup in attacks/ddos collection with provided ID.

  • gibti-get-attacks-deface-info

    Command performs Group IB event lookup in attacks/deface collection with provided ID.

  • gibti-get-available-collections

    Returns list of available collections.

  • gibti-get-compromised-account-info

    Command performs Group IB event lookup in compromised/account collection with provided ID.

  • gibti-get-compromised-breached-info

    Command performs Group IB event lookup in compromised/breached collection with provided ID.

  • gibti-get-compromised-card-group-info

    Command performs Group IB event lookup in compromised/bank_card_group collection by provided ID.

  • gibti-get-compromised-masked-card-info

    Command performs Group IB event lookup in compromised/masked_card collection by provided ID.

  • gibti-get-compromised-mule-info

    Command performs Group IB event lookup in compromised/mule collection with provided ID.

  • gibti-get-compromised-spd-info

    Command performs Group IB event lookup in compromised/spd (suspicious payment details) collection with provided ID.

  • gibti-get-malware-cnc-info

    Command performs Group IB event lookup in malware/cnc collection by provided ID.

  • gibti-get-malware-malware-info

    Command performs Group IB event lookup in malware/malware collection by provided ID.

  • gibti-get-osi-git-leak-info

    Command performs Group IB event lookup in osi/git_leak collection with provided ID.

  • gibti-get-osi-public-leak-info

    Command performs Group IB event lookup in osi/public_leak collection with provided ID.

  • gibti-get-osi-vulnerability-info

    Command performs Group IB event lookup in osi/vulnerability collection with provided ID.

  • gibti-get-phishing-group-info

    Command performs Group IB event lookup in attacks/phishing_group collection by provided ID.

  • gibti-get-suspicious-ip-open-proxy-info

    Command performs Group IB event lookup in suspicious_ip/open_proxy collection with provided ID.

  • gibti-get-suspicious-ip-scanner-info

    Command performs Group IB event lookup in suspicious_ip/scanner collection by provided ID.

  • gibti-get-suspicious-ip-socks-proxy-info

    Command performs Group IB event lookup in suspicious_ip/socks_proxy collection with provided ID.

  • gibti-get-suspicious-ip-tor-node-info

    Command performs Group IB event lookup in suspicious_ip/tor_node collection with provided ID.

  • gibti-get-suspicious-ip-vpn-info

    Command performs Group IB event lookup in suspicious_ip/vpn collection by provided ID.

  • gibti-get-threat-actor-info

    Command performs Group IB event lookup in hi/threat_actor (or in apt/threat_actor if the APT flag is true) collection with provided ID.

  • gibti-get-threat-info

    Command performs Group IB event lookup in hi/threat (or in apt/threat if the APT flag is true) collection with provided ID.

  • gibti-global-search

    Command performs global Group IB search.

  • gibti-ip-scoring

    Returns Group-IB scoring for IPs (numeric and DBotScore).

  • gibti-local-search

    Command performs Group IB search in selected collection.

  • gibtia-get-attacks-ddos-info

    Command performs Group IB event lookup in attacks/ddos collection with provided ID.

  • gibtia-get-attacks-deface-info

    Command performs Group IB event lookup in attacks/deface collection with provided ID.

  • gibtia-get-available-collections

    Returns list of available collections.

  • gibtia-get-compromised-account-info

    Command performs Group IB event lookup in compromised/account collection with provided ID.

  • gibtia-get-compromised-breached-info

    Command performs Group IB event lookup in compromised/breached collection with provided ID.

  • gibtia-get-compromised-card-group-info

    Command performs Group IB event lookup in compromised/bank_card_group collection by provided ID.

  • gibtia-get-compromised-mule-info

    Command performs Group IB event lookup in compromised/mule collection with provided ID.

  • gibtia-get-compromised-spd-info

    Command performs Group IB event lookup in compromised/spd (suspicious payment details) collection with provided ID.

  • gibtia-get-malware-cnc-info

    Command performs Group IB event lookup in malware/cnc collection by provided ID.

  • gibtia-get-malware-malware-info

    Command performs Group IB event lookup in malware/malware collection by provided ID.

  • gibtia-get-osi-git-leak-info

    Command performs Group IB event lookup in osi/git_leak collection with provided ID.

  • gibtia-get-osi-public-leak-info

    Command performs Group IB event lookup in osi/public_leak collection with provided ID.

  • gibtia-get-osi-vulnerability-info

    Command performs Group IB event lookup in osi/vulnerability collection with provided ID.

  • gibtia-get-phishing-group-info

    Command performs Group IB event lookup in attacks/phishing_group collection by provided ID.

  • gibtia-get-suspicious-ip-open-proxy-info

    Command performs Group IB event lookup in suspicious_ip/open_proxy collection with provided ID.

  • gibtia-get-suspicious-ip-scanner-info

    Command performs Group IB event lookup in suspicious_ip/scanner collection by provided ID.

  • gibtia-get-suspicious-ip-socks-proxy-info

    Command performs Group IB event lookup in suspicious_ip/socks_proxy collection with provided ID.

  • gibtia-get-suspicious-ip-tor-node-info

    Command performs Group IB event lookup in suspicious_ip/tor_node collection with provided ID.

  • gibtia-get-suspicious-ip-vpn-info

    Command performs Group IB event lookup in suspicious_ip/vpn collection by provided ID.

  • gibtia-get-threat-actor-info

    Command performs Group IB event lookup in hi/threat_actor (or in apt/threat_actor if the APT flag is true) collection with provided ID.

  • gibtia-get-threat-info

    Command performs Group IB event lookup in hi/threat (or in apt/threat if the APT flag is true) collection with provided ID.

  • gibtia-global-search

    Command performs global Group IB search.

  • gibtia-local-search

    Command performs Group IB search in selected collection.

  • ip

    Runs reputation on IPs.

import demistomock as demisto
from CommonServerPython import *
from CommonServerUserPython import *


""" IMPORTS """


import time
from json import dumps as json_dumps
from datetime import datetime

from dateparser import parse as dateparser_parse  # type: ignore[import-untyped]
from urllib3.exceptions import InsecureRequestWarning
from urllib3 import disable_warnings as urllib3_disable_warnings
from cyberintegrations import TIPoller
from cyberintegrations.utils import ParserHelper
from traceback import format_exc
import re
from enum import Enum
from itertools import chain
from collections.abc import Callable, Iterable
from typing import Any, TypeAlias, cast

# Disable insecure warnings
urllib3_disable_warnings(InsecureRequestWarning)

""" CONSTANTS """

DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
SECONDS_IN_DAY = 86_400
DEFAULT_DEDUP_LOOKBACK_DAYS = 365
# Keep the PA built-in storage key for compatibility with CommonServerPython helpers.
LAST_RUN_SEEN_INCIDENT_IDS_KEY = "found_incident_ids"
POLLER_PRODUCT_TYPE = "SOAR"
POLLER_PRODUCT_NAME = "CortexSOAR"
POLLER_INTEGRATION_NAME = "Group-IB Threat Intelligence"
POLLER_INTEGRATION_VERSION = "3.0.0"

INDICATORS_TYPES = {
    "compromised/account_group": {
        "types": {
            "event_url": "URL",
            "event_domain": "Domain",
            "events_ipv4_ip": "IP",
        },
        "add_fields_types": {
            "event_url": {},
            "event_domain": {},
            "events_ipv4_ip": {
                "asn": "asn",
                "country_name": "geocountry",
                "region": "geolocation",
            },
        },
    },
    "compromised/bank_card_group": {
        "types": {
            "cnc_url": "URL",
            "cnc_domain": "Domain",
            "cnc_ipv4_ip": "IP",
        },
        "add_fields_types": {
            "cnc_url": {},
            "cnc_domain": {},
            "cnc_ipv4_ip": {
                "cnc_ipv4_asn": "asn",
                "cnc_ipv4_country_name": "geocountry",
                "cnc_ipv4_region": "geolocation",
            },
        },
    },
    "compromised/masked_card": {
        "types": {
            "cnc_url": "URL",
            "cnc_domain": "Domain",
            "cnc_ipv4_ip": "IP",
        },
        "add_fields_types": {
            "cnc_url": {},
            "cnc_domain": {},
            "cnc_ipv4_ip": {
                "cnc_ipv4_asn": "asn",
                "cnc_ipv4_country_name": "geocountry",
                "cnc_ipv4_region": "geolocation",
            },
        },
    },
    "compromised/mule": {
        "types": {
            "cnc_url": "URL",
            "cnc_domain": "Domain",
            "cnc_ipv4_ip": "IP",
        },
        "add_fields_types": {
            "cnc_url": {},
            "cnc_domain": {},
            "cnc_ipv4_ip": {
                "cnc_ipv4_asn": "asn",
                "cnc_ipv4_country_name": "geocountry",
                "cnc_ipv4_region": "geolocation",
            },
        },
    },
    "compromised/card": {
        "types": {
            "cnc_url": "URL",
            "cnc_domain": "Domain",
            "cnc_ipv4_ip": "IP",
        },
        "add_fields_types": {
            "cnc_url": {},
            "cnc_domain": {},
            "cnc_ipv4_ip": {
                "cnc_ipv4_asn": "asn",
                "cnc_ipv4_country_name": "geocountry",
                "cnc_ipv4_region": "geolocation",
            },
        },
    },
    "compromised/spd": {
        "types": {
            "value_value": "Cryptocurrency Wallet",  # value.value as observable; type in API may vary
        },
        "add_fields_types": {
            "value_value": {
                "type": "description",  # store API type (e.g. Cryptocurrency Wallet) for context
            },
        },
    },
    "osi/vulnerability": {
        "types": {
            "id": "CVE",
        },
        "markdowns": {
            "software_mixed": (
                "| Software Name | Software Type | Software Version |\n| ------------- | ------------- | ---------------- |\n"
            )
        },
        "add_fields_types": {
            "id": {
                "cvss_score": "cvss",
                "description": "description",
                "software_mixed": "gibsoftwaremixed",
                "dateLastSeen": "cvemodified",
                "datePublished": "published",
                "severity": "severity",
            }
        },
    },
    "osi/git_repository": {
        "types": {
            "contributors_emails": "Email",
            "hash": "GIB Hash",
        },
        "add_fields_types": {"contributors_emails": {}, "hash": {}},
    },
    "attacks/phishing_kit": {
        "types": {"emails": "Email"},
        "add_fields_types": {"emails": {}},
    },
    "attacks/phishing_group": {
        "types": {
            "url": "URL",
            "phishing_domain_domain": "Domain",
            "ipv4_ip": "IP",
        },
        "add_fields_types": {
            "url": {},
            "phishing_domain_domain": {"phishing_domain_registrar": "registrarname"},
            "ipv4_ip": {
                "ipv4_country_name": "geocountry",
            },
        },
    },
    "attacks/deface": {
        "types": {"url": "URL", "target_domain": "Domain", "target_ip_ip": "IP"},
        "add_fields_types": {
            "url": {},
            "target_domain": {},
            "target_ip_ip": {
                "target_ip_asn": "asn",
                "target_ip_country_name": "geocountry",
                "target_ip_region": "geolocation",
            },
        },
    },
    "attacks/ddos": {
        "types": {"cnc_url": "URL", "cnc_domain": "Domain", "cnc_ipv4_ip": "IP"},
        "add_fields_types": {
            "cnc_url": {},
            "cnc_domain": {},
            "cnc_ipv4_ip": {
                "cnc_ipv4_asn": "asn",
                "cnc_ipv4_country_name": "geocountry",
                "cnc_ipv4_region": "geolocation",
            },
        },
    },
    "malware/cnc": {
        "types": {
            "url": "URL",
            "domain": "Domain",
        },
        "add_fields_types": {
            "url": {},
            "domain": {
                "ipv4_ip": "IP",
                "ipv4_asn": "asn",
                "country_name": "geocountry",
                "ipv4_region": "geolocation",
            },
        },
    },
    "suspicious_ip/socks_proxy": {
        "types": {"ipv4_ip": "IP"},
        "add_fields_types": {
            "ipv4_ip": {
                "ipv4_asn": "asn",
                "ipv4_country_name": "geocountry",
                "ipv4_region": "geolocation",
            }
        },
    },
    "suspicious_ip/open_proxy": {
        "types": {
            "ipv4_ip": "IP",
        },
        "add_fields_types": {
            "ipv4_ip": {
                "ipv4_asn": "asn",
                "ipv4_country_name": "geocountry",
                "ipv4_region": "geolocation",
            }
        },
    },
    "suspicious_ip/tor_node": {
        "types": {"ipv4_ip": "IP"},
        "add_fields_types": {
            "ipv4_ip": {
                "ipv4_asn": "asn",
                "ipv4_country_name": "geocountry",
                "ipv4_region": "geolocation",
            }
        },
    },
    "suspicious_ip/vpn": {
        "types": {
            "ipv4_ip": "IP",
        },
        "add_fields_types": {
            "ipv4_ip": {
                "ipv4_asn": "asn",
                "ipv4_country_name": "geocountry",
                "ipv4_region": "geolocation",
            },
        },
    },
    "suspicious_ip/scanner": {
        "types": {
            "ipv4_ip": "IP",
        },
        "add_fields_types": {
            "ipv4_ip": {
                "ipv4_asn": "asn",
                "ipv4_country_name": "geocountry",
                "ipv4_region": "geolocation",
            },
        },
    },
    "hi/threat": {
        "types": {
            "ipv4": "IP",
            "domain": "Domain",
            "url": "URL",
            "hashes_md5": "File",
        },
        "add_fields_types": {
            "ipv4": {},
            "domain": {},
            "url": {},
            "hashes_md5": {
                "name": "gibfilename",
                "hashes_md5": "md5",
                "hashes_sha1": "sha1",
                "hashes_sha256": "sha256",
                "size": "size",
            },
        },
    },
    "apt/threat": {
        "types": {
            "ipv4": "IP",
            "domain": "Domain",
            "url": "URL",
            "hashes_md5": "File",
        },
        "add_fields_types": {
            "ipv4": {},
            "domain": {},
            "url": {},
            "hashes_md5": {
                "name": "gibfilename",
                "hashes_md5": "md5",
                "hashes_sha1": "sha1",
                "hashes_sha256": "sha256",
                "size": "size",
            },
        },
    },
}

PREFIXES = {
    "compromised/account_group": "Compromised Account Group",
    "compromised/bank_card_group": "Compromised Card Group",
    "compromised/masked_card": "Compromised Masked Card",
    "compromised/breached": "Data Breach",
    "compromised/mule": "Compromised Mule",
    "compromised/spd": "Compromised SPD",
    "osi/git_repository": "Git Leak",
    "osi/public_leak": "Public Leak",
    "osi/vulnerability": "OSI Vulnerability",
    "attacks/ddos": "Attacks DDoS",
    "attacks/deface": "Attacks Deface",
    "attacks/phishing_group": "Phishing Group",
    "attacks/phishing_kit": "Phishing Kit",
    "apt/threat": "Nation-State Cybercriminals Threat Report",
    "apt/threat_actor": "Nation-State Cybercriminals Threat Actor Profile",
    "hi/threat": "GIB Cybercriminal Threat Report",
    "hi/threat_actor": "GIB Cybercriminal Threat Actor Profile",
    "suspicious_ip/tor_node": "Suspicious IP Tor Node",
    "suspicious_ip/open_proxy": "Suspicious IP Open Proxy",
    "suspicious_ip/socks_proxy": "Suspicious IP Socks Proxy",
    "suspicious_ip/vpn": "Suspicious IP VPN",
    "suspicious_ip/scanner": "Suspicious IP Scanner",
    "malware/cnc": "Malware CNC",
    "malware/malware": "Malware",
}

INCIDENT_CREATED_DATES_MAPPING = {
    "compromised/account_group": "dateFirstSeen",
    "compromised/breached": "uploadTime",
    "compromised/mule": ["dateAdd", "dateIncident"],
    "compromised/bank_card_group": ["dateFirstCompromised", "dateFirstSeen"],
    "compromised/masked_card": ["dateDetected", "dateCompromised"],
    "compromised/spd": ["firstSeenAt", "lastSeenAt", "createdAt"],
    "osi/git_repository": "dateDetected",
    "osi/public_leak": "created",
    "osi/vulnerability": "datePublished",
    "attacks/ddos": "dateReg",
    "attacks/deface": "date",
    "attacks/phishing_kit": "dateFirstSeen",
    "attacks/phishing_group": ["detected", "updated"],
    "apt/threat": "createdAt",
    "apt/threat_actor": "createdAt",
    "hi/threat": "createdAt",
    "hi/threat_actor": "createdAt",
    "suspicious_ip/tor_node": "dateFirstSeen",
    "suspicious_ip/open_proxy": "dateFirstSeen",
    "suspicious_ip/socks_proxy": "dateFirstSeen",
    "suspicious_ip/vpn": "dateFirstSeen",
    "suspicious_ip/scanner": "dateFirstSeen",
    "malware/cnc": "dateFirstSeen",
    "malware/malware": "updatedAt",
}

COLLECTIONS_THAT_MAY_NOT_SUPPORT_ID_SEARCH_VIA_UPDATED = [
    "suspicious_ip/tor_node",
    "suspicious_ip/open_proxy",
    "suspicious_ip/socks_proxy",
    "osi/public_leak",
    "attacks/phishing_group",
]

SET_WITH_ALL_DATE_FIELDS = {
    "dateEnd",
    "createdAt",
    "updated",
    "dateCreated",
    "dateFirstSeen",
    "dateModified",
    "dateLastCompromised",
    "added",
    "updatedAt",
    "created",
    "dateAdd",
    "dateBegin",
    "dateLastSeen",
    "blocked",
    "detected",
    "dateIncident",
    "dateFirstCompromised",
    "dateDetected",
    "datePublished",
    "dateReg",
    "date",
    "validThruDate",
    "datecompromised",
    "dateDetected",
}

TABLES_MAPPING = {
    "compromised/account_group": ["events_table"],
    "compromised/bank_card_group": ["threatActor", "compromised_events", "malware"],
    "compromised/spd": ["events", "sources", "malware", "threatActor"],
    "osi/git_repository": ["files"],
    "osi/public_leak": ["linkList", "matches"],
    "osi/vulnerability": ["cpeTable", "affectedSoftware"],
    "attacks/phishing_kit": ["downloadedFrom"],
    "malware/cnc": ["threatActor", "malwareList"],
    "malware/malware": ["taList"],
    "hi/threat": ["forumsAccounts"],
    "hi/threat_actor": ["reports"],
    "apt/threat_actor": ["reports"],
    "apt/threat": ["forumsAccounts"],
}

HTML_FIELDS = {
    "apt/threat_actor": ["description"],
    "apt/threat": ["description"],
    "malware/malware": ["description", "shortDescription"],
    "hi/threat": ["description"],
    "hi/threat_actor": ["description"],
    "osi/public_leak": ["data"],
}

PORTAL_LINKS = {
    "compromised/account_group": "https://tap.group-ib.com/cd/accounts?id=",
    "compromised/breached": "https://tap.group-ib.com/cd/breached?id=",
    "compromised/bank_card_group": "https://tap.group-ib.com/cd/cards?id=",
    "compromised/masked_card": "https://tap.group-ib.com/cd/cards?scope=masked_card&id=",
    "compromised/mule": "https://tap.group-ib.com/cd/mules?id=",
    "compromised/spd": "https://tap.group-ib.com/cd/suspicious-payment-details?id=",
    "hi/threat": "https://tap.group-ib.com/ta/last-threats?threat=",
    "hi/threat_actor": "https://tap.group-ib.com/ta/actors?ta=",
    "apt/threat": "https://tap.group-ib.com/ta/last-threats?threat=",
    "apt/threat_actor": "https://tap.group-ib.com/ta/actors?ta=",
    "attacks/ddos": "https://tap.group-ib.com/attacks/ddos?id=",
    "attacks/deface": "https://tap.group-ib.com/attacks/deface?q=id:",
    "attacks/phishing_group": "https://tap.group-ib.com/attacks/phishing?scope=all&q=id:",
    "attacks/phishing_kit": "https://tap.group-ib.com/malware/phishing-kit?p=1&q=",
    "malware/malware": "https://tap.group-ib.com/malware/reports/",
    "osi/git_repository": "https://tap.group-ib.com/cd/git-leaks?id=",
    "osi/public_leak": "https://tap.group-ib.com/cd/leaks?id=",
    "osi/vulnerability": "https://tap.group-ib.com/malware/vulnerabilities?p=1&scope=all&q=",
    "suspicious_ip/tor_node": "https://tap.group-ib.com/suspicious/tor?q=",
    "suspicious_ip/open_proxy": "https://tap.group-ib.com/suspicious/proxies?q=",
    "suspicious_ip/socks_proxy": "https://tap.group-ib.com/suspicious/socks?q=",
    "suspicious_ip/scanner": "https://tap.group-ib.com/suspicious/scanning?ip=",
    "suspicious_ip/vpn": "https://tap.group-ib.com/suspicious/vpn?q=",
}

COLLECTIONS_THAT_ARE_REQUIRED_HUNTING_RULES = [
    "osi/git_repository",
    "osi/public_leak",
    "compromised/breached",
]

COLLECTIONS_REQUIRING_SEARCH_VIA_QUERY_PARAMETER = [
    "osi/public_leak",
    "attacks/phishing_group",
]

COMMON_SCORE_MAP = {
    "unknown": Common.DBotScore.NONE,
    "good": Common.DBotScore.GOOD,
    "suspicious": Common.DBotScore.SUSPICIOUS,
    "bad": Common.DBotScore.BAD,
}

Reliability: TypeAlias = str

COMMON_RELIABILITY_MAP: dict[str, Reliability] = {
    "a": DBotScoreReliability.A,
    "a+": DBotScoreReliability.A_PLUS,
    "b": DBotScoreReliability.B,
    "c": DBotScoreReliability.C,
    "d": DBotScoreReliability.D,
    "e": DBotScoreReliability.E,
    "f": DBotScoreReliability.F,
}


class NumberedSeverity(Enum):
    LOW = 1
    MEDIUM = 2
    HIGH = 3


class StringSeverity(Enum):
    LOW = "Low"
    MEDIUM = "Medium"
    HIGH = "High"


COLLECTION_COMPROMISED_BREACHED_DB = "compromised/breached"
COLLECTION_COMPROMISED_SPD = "compromised/spd"
UNKNOWN_SEVERITY = "Unknown"

INCIDENT_SYSTEM_SEVERITY_MAP = {
    "green": NumberedSeverity.LOW.value,
    "orange": NumberedSeverity.MEDIUM.value,
    "red": NumberedSeverity.HIGH.value,
}

INCIDENT_CUSTOM_SEVERITY_MAP = {
    "green": StringSeverity.LOW.value,
    "orange": StringSeverity.MEDIUM.value,
    "red": StringSeverity.HIGH.value,
}


MAPPING = {
    "compromised/account_group": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "login",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "login": "login",  # GIB Compromised Login
        "password": "password",  # GIB Password
        "parsedLogin": {
            "domain": "parsedLogin.domain",  # GIB Parsed Login Domain
            "ip": "parsedLogin.ip",  # GIB Parsed Login IP
        },
        "service": {
            "domain": "service.domain",  # GIB Service Domain
            "ip": "service.ip",  # GIB Service IP
            "url": "service.url",  # GIB Service URL
        },
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("compromised/account_group"),
                "dynamic": "id",
            }
        },
        "events_table": {  # GIB Compromised Events Information Table
            "cnc": "events.cnc.cnc",
            "asn": "events.client.ipv4.asn",
            "city": "events.client.ipv4.city",
            "region": "events.client.ipv4.region",
            "provider": "events.client.ipv4.provider",
            "countryCode": "events.client.ipv4.countryCode",
            "ip": "events.client.ipv4.ip",
            "malware": "events.malware.name",
            "threatActor": "events.threatActor.name",
            "dateDetected": "events.dateDetected",
            "dateCompromised": "events.dateCompromised",
            "phone": "events.person.phone",
            "name": "events.person.name",
            "email": "events.person.email",
            "address": "events.person.address",
        },
        # END Information from Group-IB
        # Group-IB Dates
        "dateFirstCompromised": "dateFirstCompromised",  # GIB Date First Compromised
        "dateLastCompromised": "dateLastCompromised",  # GIB Date Last Compromised
        "dateFirstSeen": "dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        # END Group-IB Dates
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        "indicators": {  # GIB Related Indicators Data
            "event_url": "events.cnc.url",
            "event_domain": "events.cnc.domain",
            "events_ipv4_ip": "events.cnc.ipv4.ip",
            "asn": "events.client.ipv4.asn",
            "country_name": "events.client.ipv4.countryName",
            "region": "events.client.ipv4.region",
        },
        "source_type": "sourceType",  # Not displayed in the incident, but used in the code
    },
    "compromised/bank_card_group": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "cardInfo.number",
        # Card Info From Group-IB
        "issuer": "cardInfo.issuer.issuer",  # GIB Card Issuer
        "number": "cardInfo.number",  # GIB Card Number
        "type": "cardInfo.type",  # GIB Card Type
        "payment_system": "cardInfo.system",  # GIB Payment System
        # End Card Info From Group-IB
        # Information from Group-IB
        "id": "id",  # GIB ID
        "compromised_events": {  # GIB Compromised Events Table
            "cvv": "events.cardInfo.cvv",
            "valid_thru_date": "events.cardInfo.validThruDate",
            "valid_thru": "events.cardInfo.validThru",
            "client_ip": "events.client.ipv4.ip",
            "cnc": "events.cnc.cnc",
            "cnc_ip": "events.cnc.ipv4.ip",
            "threat_actor_name": "events.threatActor.name",
            "date_compromised": "events.dateCompromised",
            "victim_phone": "events.owner.phone",
            "victim_name": "events.owner.name",
            "malware": "events.malware.name",
        },
        "malware": {  # GIB Malware Table
            "id": "malware.id",
            "name": "malware.name",
        },
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("compromised/bank_card_group"),
                "dynamic": "id",
            }
        },
        # End Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        # Group-IB Dates
        "dateFirstSeen": "dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        "dateFirstCompromised": "dateFirstCompromised",  # GIB Date First Compromised
        "dateLastCompromised": "dateLastCompromised",  # GIB Date Last Compromised
        # END Group-IB Dates
        # Threat Actor
        "threatActor": {  # GIB Threat Actors Table
            "id": "threatActor.id",
            "name": "threatActor.name",
        },
        # End Threat Actor
        "indicators": {  # GIB Related Indicators Data
            "cnc_url": "events.cnc.url",
            "cnc_domain": "events.cnc.domain",
            "cnc_ipv4_ip": "events.cnc.ipv4.ip",
            "cnc_ipv4_asn": "events.cnc.ipv4.asn",
            "cnc_ipv4_country_name": "events.cnc.ipv4.countryName",
            "cnc_ipv4_region": "events.cnc.ipv4.region",
        },
    },
    "compromised/masked_card": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "cardInfo.number",
        "id": "id",  # GIB ID
        "baseName": "baseName",  # GIB Base Name
        # Group-IB Card Information
        "card_bin": "cardInfo.bin",  # GIB Card BIN
        "cvv": "cardInfo.cvv",  # GIB CVV
        "card_dump": "cardInfo.dump",  # GIB Card Dump
        "issuer_country_code": "cardInfo.issuer.countryCode",  # GIB Card Issuer Country Code
        "issuer_country_name": "cardInfo.issuer.countryName",  # GIB Card Issuer Country Name
        "issuer": "cardInfo.issuer.issuer",  # GIB Card Issuer
        "number": "cardInfo.number",  # GIB Card Number
        "card_pin": "cardInfo.pin",  # GIB Card PIN
        "payment_system": "cardInfo.system",  # GIB Payment System
        "card_category": "cardInfo.category",  # GIB Card Category
        "type": "cardInfo.type",  # GIB Card Type
        "validThru": "cardInfo.validThru",  # GIB Card Valid Thru
        "validThruDate": "cardInfo.validThruDate",  # GIB Card Valid Thru Date
        # END Group-IB Card Information
        # Group-IB Client Information
        "client_asn": "client.ipv4.asn",  # GIB Client ASN
        "client_city": "client.ipv4.city",  # GIB Client City
        "client_region": "client.ipv4.region",  # GIB Client Region
        "client_provider": "client.ipv4.provider",  # GIB Client Provider
        "client_country_code": "client.ipv4.countryCode",  # GIB Client Country Code
        "client_country_name": "client.ipv4.countryName",  # GIB Client Country Name
        "client_ip": "client.ipv4.ip",  # GIB Client IP
        # END Group-IB Client Information
        # Group-IB CNC Information
        "cnc": "cnc.cnc",  # GIB CNC
        "cnc_domain": "cnc.domain",  # GIB CNC Domain
        "cnc_ip": "cnc.ipv4.ip",  # GIB CNC IP
        "cnc_asn": "cnc.ipv4.asn",  # GIB CNC ASN
        "cnc_city": "cnc.ipv4.city",  # GIB CNC City
        "cnc_region": "cnc.ipv4.region",  # GIB CNC Region
        "cnc_provider": "cnc.ipv4.provider",  # GIB CNC Provider
        "cnc_country_code": "cnc.ipv4.countryCode",  # GIB CNC Country Code
        "cnc_country_name": "cnc.ipv4.countryName",  # GIB CNC Country Name
        "cnc_url": "cnc.url",  # GIB CNC URL
        # END Group-IB CNC Information
        # Group-IB Dates
        "dateDetected": "dateDetected",  # GIB Date of Detection
        "dateCompromised": "dateCompromised",  # GIB Date Compromised
        # END Group-IB Dates
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
            "ttl": "evaluation.ttl",  # GIB TTL
        },
        # END Group-IB Evaluation
        "isDump": "isDump",  # GIB Is Dump
        "isExpired": "isExpired",  # GIB Is Expired
        "isMasked": "isMasked",  # GIB Is Masked
        # Group-IB Malware Information
        "malware_name": "malware.name",  # GIB Malware Name
        "malware_id": "malware.id",  # GIB Malware ID
        "malware_stix_guid": "malware.stixGuid",  # GIB Malware STIX GUID
        # END Group-IB Malware Information
        # Group-IB Owner Information
        "address": "owner.address",  # GIB Address
        "owner_birthday": "owner.birthday",  # GIB Owner Birthday
        "owner_country_code": "owner.countryCode",  # GIB Owner Country Code
        "email": "owner.email",  # GIB Email
        "owner_name": "owner.name",  # GIB Person
        "owner_passport": "owner.passport",  # GIB Owner Passport
        "phone": "owner.phone",  # Phone Number
        "owner_state": "owner.state",  # GIB Owner State
        "owner_tax_number": "owner.taxNumber",  # GIB Owner Tax Number
        "owner_zip": "owner.zip",  # GIB Owner ZIP
        "owner_city": "owner.city",  # GIB Owner City
        # END Group-IB Owner Information
        # Group-IB Price Information
        "price_currency": "price.currency",  # GIB Price Currency
        "price_value": "price.value",  # GIB Price Value
        # END Group-IB Price Information
        "sourceLink": "sourceLink",  # GIB Source Link
        "sourceType": "sourceType",  # GIB Source
        # Group-IB Threat Actor Information
        "threat_actor_name": "threatActor.name",  # GIB Threat Actor Name
        "threat_actor_id": "threatActor.id",  # GIB Threat Actor ID
        "threat_actor_stix_guid": "threatActor.stixGuid",  # GIB Threat Actor STIX GUID
        "threat_actor_is_apt": "threatActor.isAPT",  # GIB Threat Actor is APT
        "threat_actor_country": "threatActor.country",  # GIB Threat Actor Country
        # END Group-IB Threat Actor Information
        "track": "track",  # GIB Track
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("compromised/masked_card"),
                "dynamic": "id",
            }
        },
        "indicators": {  # GIB Related Indicators Data
            "cnc_url": "cnc.url",
            "cnc_domain": "cnc.domain",
            "cnc_ipv4_ip": "cnc.ipv4.ip",
            "cnc_ipv4_asn": "cnc.ipv4.asn",
            "cnc_ipv4_country_name": "cnc.ipv4.countryName",
            "cnc_ipv4_region": "cnc.ipv4.region",
        },
    },
    "compromised/breached": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "id",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "leakName": "leakName",  # GIB Leak Name
        "passwords": "password",  # GIB Passwords
        "description": "description",  # Description
        "emails": "email",  # GIB Emails
        "emailDomains": "addInfo.emailDomain",  # GIB Email Domains
        "portalLink": "set_generated_portal_link",  # GIB Portal Link
        # END Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        # Group-IB Dates
        "leakPublished": "leakPublished",  # GIB Leak Published
        "updateTime": "updateTime",  # GIB Update Time
        "uploadTime": "uploadTime",  # GIB Upload Time
        # END Group-IB Dates
    },
    "compromised/mule": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "account",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "hash": "hash",  # GIB Data Hash
        "dateAdd": "dateAdd",  # GIB Date Add
        "dateIncident": "dateIncident",  # GIB Date Incident
        "organization": {
            "bic": "organization.bic",  # GIB Organization BIC
            "bsb": "organization.bsb",  # GIB Organization BSB
            "iban": "organization.iban",  # GIB Organization IBAN
            "name": "organization.name",  # GIB Organization Name
            "swift": "organization.swift",  # GIB Organization SWIFT
            "clabe": "organization.clabe",  # GIB Organization CLABE
        },
        "account": "account",  # GIB Compromised Account
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("compromised/mule"),
                "dynamic": "id",
            }
        },
        # END Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        "indicators": {  # GIB Related Indicators Data
            "cnc_url": "cnc.url",
            "cnc_domain": "cnc.domain",
            "cnc_ipv4_ip": "cnc.ipv4.ip",
            "cnc_ipv4_asn": "cnc.ipv4.asn",
            "cnc_ipv4_country_name": "cnc.ipv4.countryName",
            "cnc_ipv4_region": "cnc.ipv4.region",
        },
    },
    "compromised/spd": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "type",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "type": "type",  # GIB SPD Type
        "serviceType": "serviceType",  # GIB SPD Service Type
        "ownerName": "ownerName",  # GIB SPD Owner Name
        "illegalScore": "illegalScore",  # GIB SPD Illegal Score
        "value": "value.value",  # GIB SPD Value
        "createdAt": "createdAt",  # GIB Date Created At (gibdatecreatedat)
        "firstSeenAt": "firstSeenAt",  # GIB Date First Seen (gibdatefirstseen)
        "lastSeenAt": "lastSeenAt",  # GIB Date Last Seen (gibdatelastseen)
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("compromised/spd"),
                "dynamic": "id",
            }
        },
        "events": {  # GIB SPD Events Table
            "compromisedAt": "events.compromisedAt",
            "detectedAt": "events.detectedAt",
            "sourceName": "events.source.name",
            "sourceType": "events.source.type",
            "illegalScore": "events.illegalScore",
            "malwareId": "events.malware.id",
            "malwareName": "events.malware.name",
            "threatActorId": "events.threatActor.id",
            "threatActorName": "events.threatActor.name",
        },
        "sources": {  # GIB SPD Sources Table
            "name": "sources.name",
            "type": "sources.type",
        },
        "malware": {  # GIB SPD Malware Table
            "id": "malware.id",
            "name": "malware.name",
        },
        "threatActor": {  # GIB SPD Threat Actor Table
            "id": "threatActor.id",
            "name": "threatActor.name",
        },
        # END Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
            "ttl": "evaluation.ttl",  # GIB TTL
        },
        # END Group-IB Evaluation
    },
    "osi/git_repository": {  # GIB Source:sourceType, severity:systemSeverity
        # Information from Group-IB
        "id": "id",  # GIB ID
        "name": "name",
        "leaked_file_name": "name",  # GIB Leaked File Name
        "source": "source",  # GIB GIT Source
        "dateDetected": "dateDetected",  # GIB Date of Detection
        "dateCreated": "dateCreated",  # GIB Date Created
        "files": {  # GIB OSI Git Repository Files Table
            "file_id": "files.id",
            "file_name": "files.name",
            "hash": "files.revisions.hash",
            "dateCreated": "files.dateCreated",
            "dateDetected": "files.dateDetected",
            "authorName": "files.revisions.info.authorName",
            "authorEmail": "files.revisions.info.authorEmail",
            "url": "files.url",
            "dataFound": "files.dataFound",
        },
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("osi/git_repository"),
                "dynamic": "id",
            }
        },
        # END Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        "indicators": {  # GIB Related Indicators Data
            "hash": "files.revisions.hash",
            "contributors_emails": "contributors.authorEmail",
        },
    },
    "osi/public_leak": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "hash",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "hash": "hash",  # GIB Data Hash
        "created": "created",  # GIB Date Created
        "data": "data",  # GIB Leaked Data
        "linkList": {  # GIB Link List Table
            "author": "linkList.author",
            "hash": "linkList.hash",
            "link": "linkList.link",
            "title": "linkList.title",
            "source": "linkList.source",
            "dateDetected": "linkList.dateDetected",
            "datePublished": "linkList.datePublished",
        },
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("osi/public_leak"),
                "dynamic": "id",
            }
        },
        "matches": "matches",  # GIB Matches Table
        # END Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
    },
    "osi/vulnerability": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "id",
        # Group-IB Dates
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        "dateModified": "dateModified",  # GIB Date Modified
        "datePublished": "datePublished",  # GIB Date Published
        # END Group-IB Dates
        # Information from Group-IB
        "id": "id",  # GIB ID
        "bulletinFamily": "bulletinFamily",  # GIB Bulletin Family
        "description": "description",  # Description
        "extDescription": "extDescription",  # GIB Extended Description
        "reporter": "reporter",  # GIB Reporter
        "hasExploit": "hasExploit",  # GIB Has Exploit
        "href": "href",  # GIB Href
        "mergedCvss": "mergedCvss",  # GIB Merged Cvss
        "type": "type",  # GIB Vulnerability Type
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("osi/vulnerability"),
                "dynamic": "id",
            }
        },
        "cpeTable": {  # GIB CPE Table
            "product": "cpeTable.product",
            "string": "cpeTable.string",
            "string23": "cpeTable.string23",
            "type": "cpeTable.type",
            "vendor": "cpeTable.vendor",
            "version": "cpeTable.version",
        },
        # END Information from Group-IB
        # Group-IB Affected Software
        "affectedSoftware": {  # GIB Affected Software Table
            "name": "affectedSoftware.name",
            "operator": "affectedSoftware.operator",
            "version": "affectedSoftware.version",
        },
        # END Group-IB Affected Software
        # Group-IB CVSS Information
        "cvss": {
            "score": "cvss.score",  # GIB CVSS Score
            "vector": "cvss.vector",  # GIB CVSS Vector
        },
        "extCvss": {
            "base": "extCvss.base",  # GIB Extended CVSS Base
            "exploitability": "extCvss.exploitability",  # GIB Extended CVSS Exploitability
            "impact": "extCvss.impact",  # GIB Extended CVSS Impact
            "overall": "extCvss.overall",  # GIB Extended CVSS Overall
            "temporal": "extCvss.temporal",  # GIB Extended CVSS Temporal
        },
        # END Group-IB CVSS Information
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        "indicators": {  # GIB Related Indicators Data
            "severity": "evaluation.severity",
            "id": "id",
            "cvss_score": "cvss.score",
            "description": "description",
            "dateLastSeen": "dateLastSeen",
            "datePublished": "datePublished",
            "software_mixed": {
                "names": "softwareMixed.softwareName",
                "types": "softwareMixed.softwareType",
                "versions": "softwareMixed.softwareVersion",
            },
        },
    },
    "attacks/ddos": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "target.ipv4.ip",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "dateBegin": "dateBegin",  # GIB DDOS Date Begin
        "dateEnd": "dateEnd",  # GIB DDOS Date End
        "dateReg": "dateReg",  # GIB DDOS Date Registration
        "duration": "duration",  # GIB DDOS Duration
        "protocol": "protocol",  # GIB DDOS Protocol
        "source": "source",  # GIB DDOS Source
        "type": "type",  # GIB DDOS Type
        "malwareName": "malware.name",  # GIB Malware Name
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("attacks/ddos"),
                "dynamic": "id",
            }
        },
        # END Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        # CNC Information from Group-IB
        "cnc": {
            "cnc": "cnc.cnc",  # GIB CNC
            "domain": "cnc.domain",  # GIB CNC Domain
            "port": "cnc.port",  # GIB CNC Port
            "url": "cnc.url",  # GIB CNC URL
        },
        # END CNC Information from Group-IB
        # Group-IB Threat Actor
        "threatActor": {
            "id": "threatActor.id",  # GIB Threat Actor ID
            "name": "threatActor.name",  # GIB Threat Actor Name
            "isAPT": "threatActor.isAPT",  # GIB Threat Actor is APT
        },
        # End Group-IB Threat Actor
        # Group-IB DDOS Target
        "target": {
            "url": "target.url",  # GIB DDOS Target URL
            "asn": "target.ipv4.asn",  # GIB DDOS Target ASN
            "city": "target.ipv4.city",  # GIB DDOS Target City
            "region": "target.ipv4.region",  # GIB DDOS Target Region
            "provider": "target.ipv4.provider",  # GIB DDOS Target Provider
            "countryCode": "target.ipv4.countryCode",  # GIB DDOS Target Country Code
            "countryName": "target.ipv4.countryName",  # GIB DDOS Target Country Name
            "ip": "target.ipv4.ip",  # GIB DDOS Target IP
            "port": "target.port",  # GIB DDOS Target Port
            "category": "target.category",  # GIB DDOS Target Category
            "domain": "target.domain",  # GIB DDOS Target Domain
        },
        # END Group-IB DDOS Target
        # Group-IB DDOS Request
        "requestData": {
            "link": "requestData.link",  # GIB DDOS Request Data Link
            "headersHash": "requestData.headersHash",  # GIB DDOS Request Headers Hash
            "body": "requestData.body",  # GIB DDOS Request Body
            "bodyHash": "requestData.bodyHash",  # GIB DDOS Request Body Hash
        },
        # END Group-IB DDOS Request
        "indicators": {  # GIB Related Indicators Data
            "cnc_url": "cnc.url",
            "cnc_domain": "cnc.domain",
            "cnc_ipv4_ip": "cnc.ipv4.ip",
            "cnc_ipv4_asn": "cnc.ipv4.asn",
            "cnc_ipv4_country_name": "cnc.ipv4.countryName",
            "cnc_ipv4_region": "cnc.ipv4.region",
        },
    },
    "attacks/deface": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "url",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "mirrorLink": "mirrorLink",  # GIB Mirror Link
        "providerDomain": "providerDomain",  # GIB Provider Domain
        "siteUrl": "siteUrl",  # GIB Deface Site URL
        "source": "source",  # GIB Deface Source
        "targetDomain": "targetDomain",  # GIB Target Domain
        "targetDomainProvider": "targetDomainProvider",  # GIB Target Domain Provider
        "date": "date",  # GIB Deface Date
        "contacts": "contacts",  # GIB Deface Contacts
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("attacks/deface"),
                "dynamic": "id",
            }
        },
        # END Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        # Group-IB Target IP
        "targetIp": {
            "asn": "targetIp.asn",  # GIB Target ASN
            "city": "targetIp.city",  # GIB Target City
            "countryCode": "targetIp.countryCode",  # GIB Country Code
            "countryName": "targetIp.countryName",  # GIB Country Name
            "ip": "targetIp.ip",  # GIB Target IP
            "provider": "targetIp.provider",  # GIB Target Provider
            "region": "targetIp.region",  # GIB Target Region
        },
        # END Group-IB Target IP
        # Group-IB Threat Actor
        "threatActor": {
            "id": "threatActor.id",  # GIB Threat Actor ID
            "name": "threatActor.name",  # GIB Threat Actor Name
            "isAPT": "threatActor.isAPT",  # GIB Threat Actor is APT
        },
        # End Group-IB Threat Actor
        "indicators": {  # GIB Related Indicators Data
            "url": "url",
            "target_domain": "targetDomain",
            "target_ip_ip": "targetIp.ip",
            "target_ip_asn": "targetIp.asn",
            "target_ip_country_name": "targetIp.countryName",
            "target_ip_region": "targetIp.region",
        },
    },
    "attacks/phishing_group": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "brand",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "brand": "brand",  # GIB Phishing Brand
        "phishing_urls": "phishing.url",  # GIB Phishing URLs
        "objective": "objective",  # GIB Phishing Objectives
        "source": "source",  # GIB Phishing Sources
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("attacks/phishing_group"),
                "dynamic": "id",
            }
        },
        # End Information from Group-IB
        # Group-IB Dates
        "blocked": "date.blocked",  # GIB Phishing Date Blocked
        "added": "date.added",  # GIB Phishing Date Added
        "detected": "date.detected",  # GIB Phishing Date Detected
        "updated": "date.updated",  # GIB Phishing Date Updated
        # END Group-IB Dates
        # Group-IB Domain Information
        "domainInfo": {
            "domain": "domainInfo.domain",  # GIB Phishing Domain
            "domainPuny": "domainInfo.domainPuny",  # GIB Phishing Domain Puny
            "expirationDate": "domainInfo.expirationDate",  # GIB Phishing Domain Expiration Date
            "registrar": "domainInfo.registrar",  # GIB Phishing Registrar
        },
        # END Group-IB Domain Information
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        # Group-IB Phishing Information
        "phishing_ip": {  # GIB Phishing IP Table
            "ip": "ip.ip",
            "countryCode": "ip.countryCode",
            "countryName": "ip.countryName",
            "provider": "ip.provider",
        },
        # End Phishing Information from Group-IB
        # Group-IB Threat Actor Information
        "threatActor": {
            "id": "threatActor.id",  # GIB Threat Actor ID
            "name": "threatActor.name",  # GIB Threat Actor Name
        },
        # End Group-IB Threat Actor Information
        # Group-IB Phishing Kit Table
        "phishing_kit_table": {  # GIB Phishing Kit Table
            "name": "phishing.phishingKit.name",
            "email": "phishing.phishingKit.email",
        },
        # END Group-IB Phishing Kit Table
        "indicators": {  # GIB Related Indicators Data
            "url": "phishing.url",
            "phishing_domain_domain": "domain",
            "phishing_domain_registrar": "domainInfo.registrar",
            "ipv4_ip": "phishing.ip.ip",
            "ipv4_country_name": "phishing.ip.countryName",
        },
    },
    "attacks/phishing_kit": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "hash",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "hash": "hash",  # GIB Data Hash
        "dateDetected": "dateDetected",  # GIB Date of Detection
        "dateFirstSeen": "dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        "source": "source",  # GIB Phishing Kit Source
        "emails": "emails",  # GIB Phishing Kit Email
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("attacks/phishing_kit"),
                "dynamic": "id",
            }
        },
        "downloadedFrom": {  # GIB Downloaded From Table
            "date": "downloadedFrom.date",
            "url": "downloadedFrom.url",
            "phishingUrl": "downloadedFrom.phishingUrl",
            "domain": "downloadedFrom.domain",
            "fileName": "downloadedFrom.fileName",
        },
        # End Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        "indicators": {"emails": "emails"},  # GIB Related Indicators Data
    },
    "suspicious_ip/tor_node": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "ipv4.ip",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "dateFirstSeen": "dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("suspicious_ip/tor_node"),
                "dynamic": "id",
            }
        },
        # End Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        "indicators": {  # GIB Related Indicators Data
            "ipv4_ip": "ipv4.ip",
            "ipv4_asn": "ipv4.asn",
            "ipv4_country_name": "ipv4.countryName",
            "ipv4_region": "ipv4.region",
        },
    },
    "suspicious_ip/open_proxy": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "ipv4.ip",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "dateFirstSeen": "dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        "dateDetected": "dateDetected",  # GIB Date of Detection
        "port": "port",  # GIB Proxy Port
        "source": "source",  # GIB Proxy Source
        "sources": "sources",  # GIB Proxy Sources
        "type": "type",  # GIB Proxy Type
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("suspicious_ip/open_proxy"),
                "dynamic": "id",
            }
        },
        # End Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        "indicators": {  # GIB Related Indicators Data
            "ipv4_ip": "ipv4.ip",
            "ipv4_asn": "ipv4.asn",
            "ipv4_country_name": "ipv4.countryName",
            "ipv4_region": "ipv4.region",
        },
    },
    "suspicious_ip/socks_proxy": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "ipv4.ip",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "dateFirstSeen": "dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        "dateDetected": "dateDetected",  # GIB Date of Detection
        "source": "source",  # GIB Socks Proxy Source
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("suspicious_ip/socks_proxy"),
                "dynamic": "id",
            }
        },
        # End Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        "indicators": {  # GIB Related Indicators Data
            "ipv4_ip": "ipv4.ip",
            "ipv4_asn": "ipv4.asn",
            "ipv4_country_name": "ipv4.countryName",
            "ipv4_region": "ipv4.region",
        },
    },
    "suspicious_ip/vpn": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "id",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "dateFirstSeen": "dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        "sources": "sources",  # GIB VPN Sources
        "names": "names",  # GIB VPN Names
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("suspicious_ip/vpn"),
                "dynamic": "id",
            }
        },
        # End Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        "indicators": {  # GIB Related Indicators Data
            "ipv4_ip": "ipv4.ip",
            "ipv4_asn": "ipv4.asn",
            "ipv4_country_name": "ipv4.countryName",
            "ipv4_region": "ipv4.region",
        },
    },
    "suspicious_ip/scanner": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "id",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "dateFirstSeen": "dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("suspicious_ip/scanner"),
                "dynamic": "id",
            }
        },
        "categories": "categories",  # GIB Scanner Categories
        "sources": "sources",  # GIB Scanner Sources
        # End Information from Group-IB
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        "indicators": {  # GIB Related Indicators Data
            "ipv4_ip": "ipv4.ip",
            "ipv4_asn": "ipv4.asn",
            "ipv4_country_name": "ipv4.countryName",
            "ipv4_region": "ipv4.region",
        },
    },
    "malware/cnc": {  # GIB Source:sourceType
        "name": "cnc",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "cnc": "cnc",  # GIB CNC URL
        "dateFirstSeen": "dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        "dateDetected": "dateDetected",  # GIB Date of Detection
        "domain": "domain",  # GIB Malware CNC Domain
        "malwareList": {  # GIB Malware Table
            "id": "malwareList.id",
            "name": "malwareList.name",
        },
        # End Information from Group-IB
        # Group-IB Threat Actor
        "threatActor": {  # GIB Threat Actors Table
            "id": "threatActor.id",
            "name": "threatActor.name",
        },
        # End Group-IB Threat Actor
        "indicators": {  # GIB Related Indicators Data
            "url": "url",
            "domain": "domain",
            "ipv4_ip": "ipv4.ip",
            "ipv4_asn": "ipv4.asn",
            "country_name": "ipv4.countryName",
            "ipv4_region": "ipv4.region",
            "cnc": "cnc",
        },
    },
    "malware/malware": {  # GIB Source:sourceType
        # Information from Group-IB
        "id": "id",  # GIB ID
        "name": "name",
        "malware_name": "name",  # GIB Malware Name
        "updatedAt": "updatedAt",  # GIB Date Updated At
        "aliases": "aliases",  # GIB Malware Aliases
        "category": "category",  # GIB Malware Categories
        "description": "description",  # GIB Malware Description
        "shortDescription": "shortDescription",  # GIB Malware Short Description
        "geoRegion": "geoRegion",  # GIB Malware Regions
        "langs": "langs",  # GIB Malware Langs
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("malware/malware"),
                "dynamic": "id",
            }
        },
        "sourceCountry": "sourceCountry",  # GIB Malware Source Countries
        "platform": "platform",  # GIB Malware Platforms
        "threatLevel": "threatLevel",  # GIB Threat Level
        # End Information from Group-IB
        # Group-IB Threat Actor
        "taList": {  # GIB Threat Actors Table
            "id": "taList.id",
            "name": "taList.name",
        },
        # END Group-IB Threat Actor
    },
    "hi/threat": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "threatActor.name",
        # Group-IB Threat Actor
        "threatActor": {
            "country": "threatActor.country",  # GIB Threat Actor Country
            "id": "threatActor.id",  # GIB Threat Actor ID
            "isAPT": "threatActor.isAPT",  # GIB Threat Actor is APT
            "name": "threatActor.name",  # GIB Threat Actor Name
        },
        # END Group-IB Threat Actor
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        # Group-IB Cybercriminal Forum Information
        "forumsAccounts": {  # GIB Cybercriminal Forums Table
            "nickname": "forumsAccounts.nickname",
            "url": "forumsAccounts.url",
        },
        # END Group-IB Cybercriminal Forum Information
        # Information from Group-IB
        "id": "id",  # GIB ID
        "title": "title",  # GIB Cybercriminal Threat Title
        "description": "description",  # GIB Cybercriminal Threat Description
        "isTailored": "isTailored",  # GIB Is Tailored
        "expertise": "expertise",  # GIB Cybercriminal Expertises
        "regions": "regions",  # GIB Cybercriminal Regions
        "sectors": "sectors",  # GIB Cybercriminal Sectors
        "reportNumber": "reportNumber",  # GIB Report Number
        # Group-IB Dates
        "createdAt": "createdAt",  # GIB Date Created At
        "dateFirstSeen": "dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "dateLastSeen",  # GIB Date Last Seen
        # END Group-IB Dates
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("hi/threat"),
                "dynamic": "id",
            }
        },
        # End Information from Group-IB
        "indicators": {  # GIB Related Indicators Data
            "ipv4": "indicators.params.ipv4",
            "domain": "indicators.params.domain",
            "url": "indicators.params.url",
            "hashes_md5": "indicators.params.hashes.md5",
            "name": "indicators.params.name",
            "hashes_sha1": "indicators.params.hashes.sha1",
            "hashes_sha256": "indicators.params.hashes.sha256",
            "size": "indicators.params.size",
        },
    },
    "hi/threat_actor": {  # GIB Source:sourceType
        # Information from Group-IB
        "name": "name",
        "id": "id",  # GIB ID
        "aliases": "aliases",  # GIB Cybercriminal Threat Actor Aliases
        "description": "description",  # GIB Cybercriminal Threat Actor Description
        "isAPT": "isAPT",  # GIB Threat Actor is APT
        "threat_actor_name": "name",  # GIB Threat Actor Name
        "expertise": "stat.expertise",  # GIB Cybercriminal Expertises
        "regions": "stat.regions",  # GIB Cybercriminal Regions
        "sectors": "stat.sectors",  # GIB Cybercriminal Sectors
        "malware": "stat.malware",  # GIB Cybercriminal Malware
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("hi/threat_actor"),
                "dynamic": "id",
            }
        },
        # End Information from Group-IB
        # Group-IB Threat Actor Reports
        "reports": {  # GIB Cybercriminal Threat Actor Reports Table
            "id": "stat.reports.id",
            "name": "stat.reports.name.en",
            "datePublished": "stat.reports.datePublished",
        },
        # END Group-IB Threat Actor Reports
        # Group-IB Dates
        "createdAt": "createdAt",  # GIB Date Created At
        "updatedAt": "updatedAt",  # GIB Date Updated At
        "dateFirstSeen": "stat.dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "stat.dateLastSeen",  # GIB Date Last Seen
        # END Group-IB Dates
    },
    "apt/threat_actor": {  # GIB Source:sourceType
        # Information from Group-IB
        "name": "name",
        "id": "id",  # GIB ID
        "aliases": "aliases",  # GIB Nation-State Cybercriminals Threat Actor Aliases
        "country": "country",  # GIB Nation-State Cybercriminals Threat Actor Country
        "description": "description",  # GIB Nation-State Cybercriminals Threat Actor Description
        "goals": "goals",  # GIB Nation-State Cybercriminals Threat Actor Goals
        "isAPT": "isAPT",  # GIB Threat Actor is APT
        "labels": "labels",  # GIB Nation-State Cybercriminals Threat Actor Labels
        "threat_actor_name": "name",  # GIB Threat Actor Name
        "roles": "roles",  # GIB Nation-State Cybercriminals Threat Actor Roles
        "cve": "stat.cve",  # GIB Nation-State Cybercriminals Threat Actor CVE
        "expertise": "stat.expertise",  # GIB Nation-State Cybercriminals Expertises
        "malware": "stat.malware",  # GIB Nation-State Cybercriminals Malware
        "regions": "stat.regions",  # GIB Nation-State Cybercriminals Regions
        "sectors": "stat.sectors",  # GIB Nation-State Cybercriminals Sectors
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("apt/threat_actor"),
                "dynamic": "id",
            }
        },
        # End Information from Group-IB
        # Group-IB Threat Actor Reports
        "reports": {  # GIB Nation-State Cybercriminals Threat Actor Reports Table
            "id": "stat.reports.id",
            "name": "stat.reports.name.en",
            "datePublished": "stat.reports.datePublished",
        },
        # END Group-IB Threat Actor Reports
        # Group-IB Dates
        "createdAt": "createdAt",  # GIB Date Created At
        "dateFirstSeen": "stat.dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "stat.dateLastSeen",  # GIB Date Last Seen
        "updatedAt": "updatedAt",  # GIB Date Updated At
        # END Group-IB Dates
    },
    "apt/threat": {  # GIB Source:sourceType, severity:systemSeverity
        "name": "threatActor.name",
        # Information from Group-IB
        "id": "id",  # GIB ID
        "title": "title",  # GIB Nation-State Cybercriminals Threat Title
        "countries": "countries",  # GIB Nation-State Cybercriminals Threat Countries
        "description": "description",  # GIB Nation-State Cybercriminals Threat Description
        "expertise": "expertise",  # GIB Nation-State Cybercriminals Threat Expertises
        "isTailored": "isTailored",  # GIB Is Tailored
        "labels": "labels",  # GIB Nation-State Cybercriminals Threat Actor Labels
        "langs": "langs",  # GIB Nation-State Cybercriminals Threat Langs
        "regions": "regions",  # GIB Nation-State Cybercriminals Threat Regions
        "reportNumber": "reportNumber",  # GIB Nation-State Cybercriminals Threat Report Number
        "sectors": "sectors",  # GIB Nation-State Cybercriminals Threat Sectors
        "portalLink": {  # GIB Portal Link
            "__concatenate": {
                "static": PORTAL_LINKS.get("apt/threat"),
                "dynamic": "id",
            }
        },
        # End Information from Group-IB
        # Group-IB Dates
        "createdAt": "createdAt",  # GIB Date Created At
        "dateFirstSeen": "stat.dateFirstSeen",  # GIB Date First Seen
        "dateLastSeen": "stat.dateLastSeen",  # GIB Date Last Seen
        "datePublished": "datePublished",  # GIB Date Published
        # END Group-IB Dates
        # Group-IB Threat Actor
        "threatActor": {
            "country": "threatActor.country",  # GIB Threat Actor Country
            "id": "threatActor.id",  # GIB Threat Actor ID
            "isAPT": "threatActor.isAPT",  # GIB Threat Actor is APT
            "name": "threatActor.name",  # GIB Threat Actor Name
        },
        # END Group-IB Threat Actor
        # Group-IB Evaluation
        "evaluation": {
            "admiraltyCode": "evaluation.admiraltyCode",  # GIB Admiralty Code
            "credibility": "evaluation.credibility",  # GIB Credibility
            "reliability": "evaluation.reliability",  # GIB Reliability
            "severity": "evaluation.severity",  # GIB Severity
            "tlp": "evaluation.tlp",  # GIB TLP
        },
        # END Group-IB Evaluation
        # Group-IB Nation-State Cybercriminal Forum Information
        "forumsAccounts": {  # GIB Nation-State Cybercriminal Forums Table
            "nickname": "forumsAccounts.nickname",
            "url": "forumsAccounts.url",
        },
        # END Group-IB Cybercriminal Forum Information
        "indicators": {  # GIB Related Indicators Data
            "ipv4": "indicators.params.ipv4",
            "domain": "indicators.params.domain",
            "url": "indicators.params.url",
            "hashes_md5": "indicators.params.hashes.md5",
            "name": "indicators.params.name",
            "hashes_sha1": "indicators.params.hashes.sha1",
            "hashes_sha256": "indicators.params.hashes.sha256",
            "size": "indicators.params.size",
        },
    },
}

DEPRECATED_COLLECTIONS = {
    "malware/targeted_malware": "malware/malware",
    "compromised/masked_cards": "compromised/masked_card",
    "compromised/bank_card": "compromised/bank_card_group",
    "compromised/card": "compromised/bank_card_group",
    "compromised/account": "compromised/account_group",
    "attacks/phishing": "attacks/phishing_group",
}

REMOVED_COLLECTIONS = ["bp/phishing", "bp/phishing_kit", "compromised/imei"]


class Client(BaseClient):
    """
    Client will implement the service API, and should not contain any Demisto logic.
    Should only do requests and return data.
    """

    limit = 100

    def __init__(self, base_url, verify=True, proxy=False, headers=None, auth=None, limit: int = 100):
        super().__init__(base_url=base_url, verify=verify, proxy=proxy, headers=headers, auth=auth)

        self._auth: tuple[str, str]
        self.poller = TIPoller(
            username=self._auth[0],
            api_key=self._auth[1],
            api_url=base_url,
        )
        self.limit = int(limit)
        self._available_collections: frozenset[str] | None = None
        self.poller.set_product(
            product_type=POLLER_PRODUCT_TYPE,
            product_name=POLLER_PRODUCT_NAME,
            product_version=demisto.demistoVersion().get("version", "unknown"),
            integration_name=POLLER_INTEGRATION_NAME,
            integration_version=POLLER_INTEGRATION_VERSION,
        )

    def get_available_collections_cached(self) -> frozenset[str]:
        if self._available_collections is None:
            self._available_collections = frozenset(self.poller.get_available_collections())
        return self._available_collections

    @staticmethod
    def handle_first_time_fetch(kwargs: dict[str, Any]) -> tuple[str, str | None]:
        """
        Handle first time fetch
        """
        date_from = None
        last_fetch = kwargs.get("last_fetch")
        if not last_fetch:
            date_from = dateparser_parse(date_string=kwargs.get("first_fetch_time"))  # type: ignore
            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")  # type: ignore
        demisto.debug(
            "[handle_first_time_fetch] Computed initial parameters: "
            f"last_fetch_exists={bool(last_fetch)}, date_from={date_from}"
        )

        return last_fetch, date_from  # type: ignore

    def create_poll_generator(
        self,
        collection_name: str,
        hunting_rules: int,
        enable_probable_corporate_access: bool,
        unique: bool,
        combolist: bool,
        **kwargs,
    ):
        """
        Interface to work with different types of indicators.
        """

        last_fetch, date_from = Client.handle_first_time_fetch(kwargs)

        if collection_name == COLLECTION_COMPROMISED_BREACHED_DB:
            hunting_rules = 1

            # Keep BC with old last_fetch values that may not yet be stored as a dict.
            if last_fetch and isinstance(last_fetch, dict):
                starting_date_from = last_fetch.get("starting_date_from")
                starting_date_to = last_fetch.get("starting_date_to")
                date_to = last_fetch.get("current_date_to")
            else:
                starting_date_from = date_from
                starting_date_to = datetime.now().strftime(DATE_FORMAT)
                date_to = starting_date_to

            demisto.debug(
                "[create_poll_generator] Using search generator for compromised/breached: "
                f"last_fetch={last_fetch}, date_from={date_from}, date_to={date_to}, "
                f"starting_date_from={starting_date_from}, starting_date_to={starting_date_to}"
            )

            return self.poller.create_search_generator(
                collection_name=collection_name,
                date_from=date_from,
                date_to=date_to,
                limit=self.limit,
                apply_hunting_rules=hunting_rules,
            ), {
                "starting_date_from": starting_date_from,
                "starting_date_to": starting_date_to,
                "current_date_to": date_to,
            }

        if collection_name in COLLECTIONS_THAT_ARE_REQUIRED_HUNTING_RULES:
            hunting_rules = 1
        sequpdate_for_generator = last_fetch
        date_from_for_generator = date_from
        if not last_fetch and date_from:
            try:
                demisto.debug(
                    "[create_poll_generator] Resolving initial seqUpdate via sequence_list: "
                    f"collection={collection_name}, date_from={date_from}, hunting_rules={hunting_rules}"
                )
                seq_map = self.poller.get_seq_update_dict(
                    date=date_from,
                    collection_name=collection_name,
                    apply_hunting_rules=hunting_rules,
                )
                resolved_seq = seq_map.get(collection_name)
                if resolved_seq:
                    sequpdate_for_generator = resolved_seq
                    date_from_for_generator = None
                    demisto.debug(f"[create_poll_generator] Using resolved seqUpdate={resolved_seq}; dropping date_from")
                else:
                    demisto.debug("[create_poll_generator] sequence_list returned empty for collection; fallback to date_from")
            except Exception as e:
                demisto.debug(f"[create_poll_generator] sequence_list resolution failed: {e}; fallback to date_from")

        demisto.debug(
            "[create_poll_generator] Using update generator: "
            f"collection={collection_name}, sequpdate={sequpdate_for_generator}, date_from={date_from_for_generator}, "
            f"limit={self.limit}, hunting_rules={hunting_rules}"
        )

        return (
            self.poller.create_update_generator(
                collection_name=collection_name,
                date_from=date_from_for_generator,
                sequpdate=sequpdate_for_generator,
                limit=self.limit,
                apply_hunting_rules=hunting_rules,
                probable_corporate_access=int(enable_probable_corporate_access),
                unique=int(unique),
                combolist=int(combolist),
            ),
            sequpdate_for_generator,
        )

    def search_proxy_function(self, query: str) -> list[dict[str, Any]]:
        return self.poller.global_search(query=query)

    def get_available_collections_proxy_function(self) -> list:
        return list(self.get_available_collections_cached())


""" Support functions """


class CommonHelpers:
    @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 = {
            k: v if isinstance(v, list) else [v]  # type: ignore
            for k, v in input_dict.items()
        }

        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 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 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 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 safe_json_one_line(obj: Any) -> str:
        """
        Serialize an object to a single-line JSON string for safe War Room/context rendering.
        Falls back to `str(obj)` if JSON serialization fails.
        """
        try:
            return json.dumps(obj, ensure_ascii=False, separators=(",", ":"), default=str)
        except Exception:
            return str(obj)

    @staticmethod
    def date_parse(date: str, arg_name: str) -> str:
        date_from_parsed = dateparser_parse(date)
        if date_from_parsed is None:
            raise DemistoException(
                f"Inappropriate {arg_name} format, please use something like this: 2020-01-01 or January 1 2020"
            )
        date_from_parsed = date_from_parsed.strftime(DATE_FORMAT)
        return date_from_parsed

    @staticmethod
    def remove_html_tags(entry: dict, collection_name: str) -> dict:
        if collection_name in HTML_FIELDS:
            fields = HTML_FIELDS.get(collection_name, [])
            for field in fields:
                entry_field_value = entry.get(field, None)
                if isinstance(entry_field_value, str):
                    entry_field_value = re.sub(r"<[^>]+>", "", entry_field_value)
                    entry[field] = entry_field_value

        return entry

    @staticmethod
    def transform_list_to_str(data: list[dict]) -> list[dict]:
        for item in data:
            if isinstance(item, dict):
                for key, value in item.items():
                    if isinstance(value, list):
                        item[key] = ", ".join(str(v) for v in value)
        return data

    @staticmethod
    def custom_generate_portal_link(collection_name: str, incident: dict) -> dict:
        if collection_name != COLLECTION_COMPROMISED_BREACHED_DB:
            return incident

        emails = incident.get("emails")
        if isinstance(emails, list) and emails:
            incident["portalLink"] = PORTAL_LINKS.get(COLLECTION_COMPROMISED_BREACHED_DB, "") + str(emails[0])

        return incident

    @staticmethod
    def validate_collections(collection_name):
        if collection_name in DEPRECATED_COLLECTIONS:
            raise Exception(f"Collection {collection_name} is obsolete. Please use {DEPRECATED_COLLECTIONS.get(collection_name)}")
        if collection_name in REMOVED_COLLECTIONS:
            raise Exception(f"The {collection_name} collection is not valid")


class IndicatorsHelper:
    @staticmethod
    def check_empty_list(add_fields: dict) -> bool:
        dict_len = len(add_fields)
        empty_found_count = 0
        for _key, value in add_fields.items():
            if isinstance(value, list) and len(value) < 1:
                empty_found_count += 1

        return dict_len == empty_found_count

    @staticmethod
    def parse_to_outputs(value: str | None | list, indicator_type: str, fields: dict) -> Any:
        def calculate_dbot_score(type_):
            severity = fields.get("evaluation", {}).get("severity")
            if severity == "green":
                score = Common.DBotScore.GOOD
            elif severity == "orange":
                score = Common.DBotScore.SUSPICIOUS
            elif severity == "red":
                score = Common.DBotScore.BAD
            else:
                score = Common.DBotScore.NONE

            return Common.DBotScore(
                indicator=value,
                indicator_type=type_,
                integration_name="GIB TI",
                score=score,
            )

        indicator: Any = None
        if (value is not None or len(fields) > 0) and IndicatorsHelper.check_empty_list(fields) is False:
            if indicator_type == "IP":
                indicator = Common.IP(
                    ip=value,
                    asn=fields.get("asn"),
                    geo_country=fields.get("geocountry"),
                    geo_description=fields.get("geolocation"),
                    dbot_score=calculate_dbot_score(DBotScoreType.IP),
                )
            elif indicator_type == "Domain":
                indicator = Common.Domain(
                    domain=value,
                    registrar_name=fields.get("registrarname"),
                    dbot_score=calculate_dbot_score(DBotScoreType.DOMAIN),
                )
            elif indicator_type == "File":
                indicator = Common.File(
                    md5=value,
                    sha1=fields.get("sha1"),
                    sha256=fields.get("sha256"),
                    name=fields.get("gibfilename"),
                    size=fields.get("size"),
                    dbot_score=calculate_dbot_score(DBotScoreType.FILE),
                )
            elif indicator_type == "URL":
                indicator = Common.URL(
                    url=value,
                    dbot_score=calculate_dbot_score(DBotScoreType.URL),
                )
            elif indicator_type == "CVE":
                indicator = Common.CVE(
                    id=value,
                    cvss=fields.get("cvss"),
                    published=fields.get("published"),
                    modified=fields.get("cvemodified"),
                    description=fields.get("cvedescription"),
                )
        return indicator

    @staticmethod
    def find_iocs_in_feed(feed: str | dict[Any, Any], collection_name: str) -> list:
        """
        Finds IOCs in the feed and transform them to the appropriate format to ingest them into Demisto.

        :param feed: feed from GIB TI.
        :param collection_name: which collection this feed belongs to.
        """

        indicators = []
        if isinstance(feed, dict) and feed.get("indicators", None) is not None:
            indicator_types: dict = INDICATORS_TYPES.get(collection_name, {}).get("types", {})  # type: ignore
            add_fields_types: dict = INDICATORS_TYPES.get(collection_name, {}).get("add_fields_types", {})  # type: ignore
            feed_indicators: dict = feed["indicators"]
            feed_indicators.update({"severity": feed.get("evaluation", {}).get("severity")})

            for indicator_type_name, indicator_type in indicator_types.items():
                add_fields = {}
                indicator_value = feed_indicators.get(indicator_type_name)
                if indicator_type_name in add_fields_types:
                    for (
                        additional_field_name,
                        additional_field_type,
                    ) in add_fields_types.get(indicator_type_name).items():  # type: ignore
                        additional_field_value = feed_indicators.get(additional_field_name)
                        if additional_field_value is not None:
                            add_fields.update({additional_field_type: additional_field_value})

                output = IndicatorsHelper.parse_to_outputs(indicator_value, indicator_type, add_fields)
                if output:
                    if len(add_fields) > 0:
                        add_fields.update({"severity": feed.get("evaluation", {}).get("severity")})
                    results = [
                        CommandResults(
                            readable_output=tableToMarkdown(
                                f"{indicator_type} indicator",
                                {"value": indicator_value, **add_fields},
                            ),
                            indicator=output,
                            ignore_auto_extract=True,
                        )
                    ]
                    indicators.append(results)

        return indicators

    @staticmethod
    def dbot_from_score(score: Any) -> int:
        """
        Convert numeric Group-IB riskScore (0..100) into XSOAR DBotScore.

        Mapping:
        - None / out of range -> NONE (Unknown)
        - 0..49 -> GOOD
        - 50..84 -> SUSPICIOUS
        - 85..100 -> BAD
        """
        if score is None:
            return Common.DBotScore.NONE
        if 0 <= score <= 49:
            return Common.DBotScore.GOOD
        if 50 <= score <= 84:
            return Common.DBotScore.SUSPICIOUS
        if 85 <= score <= 100:
            return Common.DBotScore.BAD
        return Common.DBotScore.NONE

    @staticmethod
    def collect_portions_for_indicator(
        indicator_name: str,
        indicator_value: str,
        path: str,
        poller: Any,
        dates_mapping: dict[str, dict[str, str]] | None,
        sensitive_collections: list[str] | None,
    ) -> list:
        """Collect parsed portions for a given path.

        Tolerant to the Group-IB API returning a null body (no matches) and
        to malformed portion objects: any of those collapses to an empty
        result, never an exception.
        """
        portions = poller.create_update_generator(collection_name=path, query=indicator_value)
        if portions is None:
            return []
        portions_data: list[Any] = []
        use_dates = path in (sensitive_collections or [])
        for portion in portions:
            if portion is None:
                continue
            if use_dates and dates_mapping:
                parsed_portion = portion.parse_portion(keys=dates_mapping.get(path))
            else:
                parsed_portion = getattr(portion, "raw_dict", None)
            if parsed_portion in (None, "", []):
                continue
            cleaned_feed = parsed_portion[0] if isinstance(parsed_portion, list) else parsed_portion  # type: ignore
            portions_data.append(cleaned_feed)
        return portions_data

    @staticmethod
    def build_ip_enrichment(
        poller: Any,
        indicator_value: str,
        mapping: dict[str, Any],
    ) -> dict[str, Any]:
        """Build scoring and graph IP enrichment block.

        Each upstream call (scoring, graph_ip_search) may legitimately
        return a 200/null response when the IP is unknown to Group-IB.
        We swallow these as "no data" rather than letting them bubble up
        as `AttributeError: 'NoneType' object has no attribute 'get'`.
        """
        data: dict[str, Any] = {}
        try:
            scoring = poller.scoring(indicator_value) or {}
            items = (scoring or {}).get("items") or {}
            score = (items.get(indicator_value) or {}).get("riskScore")
            data.update({"scoring": {"score": score}})
        except Exception as exc:
            demisto.debug(f"[scoring] failed for {indicator_value}: {exc}")
            data.update({"scoring": {"score": None}})

        try:
            graph_ip = poller.graph_ip_search(indicator_value)
            if graph_ip:
                graph_data = ParserHelper.find_by_template(graph_ip, keys=mapping)
                data.update({"graph_ip": graph_data})
        except Exception as e:
            demisto.debug(f"[graph_ip_search] failed for {indicator_value}: {e}")
        return data

    @staticmethod
    def parse_source_reliability(value: str | None) -> Reliability | None:
        """
        Parse Source Reliability parameter (e.g. 'A - Completely reliable') into DBotScoreReliability.
        Returns None if missing or unrecognized.
        """
        if not value:
            return None
        token = value.split()[0].strip().lower()
        return COMMON_RELIABILITY_MAP.get(token)


class IncidentBuilder:
    fields_list_for_parse = [
        "creationdate",
        "firstseenbysource",
        "lastseenbysource",
        "gibdatecompromised",
    ]

    def __init__(self, collection_name: str, incident: dict, mapping: dict) -> None:
        self.collection_name = collection_name
        self.incident = incident
        self.mapping = mapping

    def get_system_severity(self) -> int:
        severity = self.incident.get("evaluation", {}).get("severity")
        return INCIDENT_SYSTEM_SEVERITY_MAP.get(severity, 0)

    def get_incident_created_time(self) -> str:
        last_exception = None
        incident_id = self.incident.get("id", None)
        occurred_date_field = INCIDENT_CREATED_DATES_MAPPING.get(self.collection_name, "-")

        if isinstance(occurred_date_field, str):
            occurred_date_field = [occurred_date_field]

        if not isinstance(occurred_date_field, list):
            raise DemistoException(f"Expected list or string for occurred_date_field, got {type(occurred_date_field).__name__}")

        for variant in occurred_date_field:
            try:
                date_value = self.incident.get(variant, "")

                if date_value is None:
                    continue
                if not isinstance(date_value, str):
                    date_value = str(date_value)
                if not date_value.strip():
                    continue
                incident_occurred_date = dateparser_parse(date_string=date_value)

                assert incident_occurred_date is not None, (
                    f"{self.incident} incident_occurred_date cannot be None, "
                    f"occurred_date_field: {variant}, incident_occurred_date: {incident_occurred_date}"
                    f"{self.collection_name} {incident_id}"
                )
                return incident_occurred_date.strftime(DATE_FORMAT)
            except AssertionError as e:
                last_exception = e

        raise AssertionError(
            f"None of the date fields {occurred_date_field} returned a valid date."
            f"Last error: {last_exception} {self.collection_name} {incident_id}"
        )

    def get_incident_name(self) -> str:
        name = ""
        prefix = PREFIXES.get(self.collection_name, "")
        if self.collection_name == COLLECTION_COMPROMISED_BREACHED_DB:
            names = self.incident["name"]
            if not isinstance(names, list):
                names = [names]
            name = f"{prefix}: " + ", ".join(names)
        elif self.collection_name == COLLECTION_COMPROMISED_SPD:
            # name = type + value
            ptype = self.incident.get("type") or "Payment data"
            value_str = self.incident.get("value")
            name = f"{prefix}: {ptype} {value_str}"
        else:
            name = f"{prefix}: {self.incident['name']}"

        return name

    def set_custom_severity(self):
        severity = self.incident.get("evaluation", {}).get("severity")
        if severity:
            self.incident["evaluation"]["severity"] = INCIDENT_CUSTOM_SEVERITY_MAP.get(severity, UNKNOWN_SEVERITY)

    @staticmethod
    def date_conversion(date: str):
        try:
            date_obj = datetime.strptime(date, "%Y-%m-%d")
            return date_obj.isoformat()
        except ValueError:
            try:
                datetime.fromisoformat(date)
                return None
            except ValueError:
                raise ValueError(f"Invalid date format provided: {date}")

    def check_dates(self):
        for field, value in self.incident.items():
            if field in SET_WITH_ALL_DATE_FIELDS and value is not None:
                new_value = self.date_conversion(value)
                if new_value:
                    self.incident[field] = new_value

    def osi_public_leak_matches_transform_to_grid_table(self, field: str):
        field_data = self.incident.get(field, {})
        if field_data:
            new_matches = []
            if isinstance(field_data, list):
                field_data = {}
            for type_, sub_dict in field_data.items():
                for sub_type, sub_list in sub_dict.items():
                    for value in sub_list:
                        new_matches.append({"type": type_, "sub_type": sub_type, "value": value})

            transformed_and_replaced_empty_values_data = CommonHelpers.replace_empty_values(new_matches)
            clean_data = CommonHelpers.remove_underscore_and_lowercase_keys(
                transformed_and_replaced_empty_values_data  # type: ignore
            )
            self.incident[field] = clean_data

    def transform_fields_to_grid_table(self):
        fields_for_modify_in_table = TABLES_MAPPING.get(self.collection_name, [])

        if fields_for_modify_in_table:
            for field in fields_for_modify_in_table:
                if self.collection_name == "osi/public_leak" and field == "matches":
                    self.osi_public_leak_matches_transform_to_grid_table(field=field)
                else:
                    field_data = self.incident.get(field, {})

                    if field_data and CommonHelpers.all_lists_empty(field_data) is False:
                        transformed_data = CommonHelpers.transform_dict(input_dict=field_data)
                        if self.collection_name == "osi/git_repository" and field == "files":
                            transformed_data = CommonHelpers.transform_list_to_str(transformed_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
                        )
                        # SPD events: show malware/threatActor id and name as strings (e.g. "MalwareName" not ["MalwareName"])
                        if self.collection_name == COLLECTION_COMPROMISED_SPD and field == "events":
                            clean_data = CommonHelpers.transform_list_to_str(clean_data)

                        self.incident[field] = clean_data
                    else:
                        self.incident[field] = None

    def build_incident(self) -> dict:
        self.incident = CommonHelpers.custom_generate_portal_link(collection_name=self.collection_name, incident=self.incident)
        incident_name = self.get_incident_name()
        system_severity = self.get_system_severity()
        self.incident.update(
            {
                "name": incident_name,
                "gibType": self.collection_name,
                "systemSeverity": system_severity,
            }
        )

        if self.collection_name == "compromised/masked_card":
            if isinstance(self.incident.get("track"), list):
                self.incident["track"] = ", ".join(str(value) for value in self.incident["track"])
            if isinstance(self.incident.get("card_bin"), list):
                self.incident["card_bin"] = ", ".join(str(value) for value in self.incident["card_bin"])

        self.set_custom_severity()
        self.check_dates()
        self.transform_fields_to_grid_table()
        self.incident = CommonHelpers.remove_html_tags(self.incident, self.collection_name)
        data = {
            "name": self.incident["name"],
            "occurred": self.get_incident_created_time(),
            "rawJSON": json_dumps(self.incident),
            "dbotMirrorId": self.incident.get("id"),
        }
        return data


class BuilderCommandResponses:
    dont_need_transformations = [COLLECTION_COMPROMISED_BREACHED_DB]

    def __init__(self, client: Client, collection_name: str, args: dict) -> None:
        self.client = client
        self.collection_name = collection_name
        self.args = args

    def transform_additional_fields_to_markdown_tables(self, feed: dict):
        additional_tables = []
        delete_keys = []
        for key, value in feed.items():
            if key not in ("evaluation", "indicators") and isinstance(value, dict):
                additional_data = CommonHelpers.transform_dict(value)
                for index, item in enumerate(additional_data):
                    table = self.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

    def get_feed(self) -> dict:
        id_ = str(self.args.get("id"))
        cleaned_feed = {}
        if self.collection_name in COLLECTIONS_THAT_MAY_NOT_SUPPORT_ID_SEARCH_VIA_UPDATED:
            if self.collection_name in COLLECTIONS_REQUIRING_SEARCH_VIA_QUERY_PARAMETER:
                query = f"id:{id_}"
            else:
                query = id_
            portions = self.client.poller.create_update_generator(collection_name=self.collection_name, query=query)
            for portion in portions:
                parsed_portion = portion.parse_portion(keys=MAPPING.get(self.collection_name, {}))
                cleaned_feed = parsed_portion[0] if isinstance(parsed_portion, list) else parsed_portion  # type: ignore

        else:
            result = self.client.poller.search_feed_by_id(self.collection_name, id_)
            mapping = MAPPING.get(self.collection_name, {})
            # This was done because the response when receiving a single record can
            # differentiate your json from getting the whole list
            if self.collection_name == "compromised/breached":
                mapping["emailDomains"] = "emails"

            parsed_portion = result.parse_portion(keys=mapping)
            cleaned_feed = parsed_portion[0] if isinstance(parsed_portion, list) else parsed_portion  # type: ignore

        return cleaned_feed  # type: ignore

    def get_indicators(self, feed: dict[Any, Any]) -> tuple[list[CommandResults] | list, dict[Any, Any]]:
        indicators = []
        indicators = IndicatorsHelper.find_iocs_in_feed(feed=feed, collection_name=self.collection_name)

        return indicators, feed

    def get_table_data(
        self,
        feed: dict[Any, Any],
    ):
        if self.collection_name in self.dont_need_transformations:
            return feed, []
        main_table_data, additional_tables = self.transform_additional_fields_to_markdown_tables(feed)
        return main_table_data, additional_tables

    def get_human_readable_feed(self, table: dict[Any, Any], name: str):
        return tableToMarkdown(
            name=name,
            t=table,
            removeNull=True,
        )

    def build_feed(self):
        feed = self.get_feed()
        feed = CommonHelpers.custom_generate_portal_link(collection_name=self.collection_name, incident=feed)
        indicators, feed = self.get_indicators(feed=feed)
        main_table_data, additional_tables = self.get_table_data(feed=feed)
        feed_id = feed.get("id")
        readable_output = self.get_human_readable_feed(table=feed, name=f"Feed from {self.collection_name} with ID {feed_id}")
        return feed, main_table_data, additional_tables, indicators, readable_output


""" Commands """


def _parse_seq_update(value: Any) -> int | None:
    if value is None:
        return None
    if isinstance(value, bool):
        return None
    if isinstance(value, int):
        return value
    if isinstance(value, float):
        return int(value)
    if isinstance(value, str):
        stripped = value.strip()
        if stripped.isdigit():
            return int(stripped)
    return None


def _serialize_seq_update(value: int) -> str:
    return str(value)


def _convert_dedup_lookback_days_to_seconds(dedup_lookback_days: int) -> int:
    """
    Convert the user-facing `dedup_lookback_days` parameter into seconds.

    The integration owns its own cache cleanup (see `_prune_seen_incident_ids`)
    instead of relying on `CommonServerPython.remove_old_incidents_ids`,
    which applies a hidden `* 2` multiplier and keeps the latest IDs forever.
    Owning the conversion guarantees a 1:1 contract: an ID added today is
    dropped exactly `dedup_lookback_days` days later.
    """
    return dedup_lookback_days * SECONDS_IN_DAY


def _get_dedup_lookback_days_from_params(params: dict) -> int:
    dedup_lookback_days = params.get("dedup_lookback_days")
    if dedup_lookback_days in (None, ""):
        return DEFAULT_DEDUP_LOOKBACK_DAYS

    if isinstance(dedup_lookback_days, bool):
        raise ValueError("dedup_lookback_days must be an integer number of days.")

    if isinstance(dedup_lookback_days, int):
        return dedup_lookback_days

    if isinstance(dedup_lookback_days, str):
        return int(dedup_lookback_days)

    raise ValueError("dedup_lookback_days must be a string or integer value.")


def _is_fetch_dedup_enabled(skip_updated_incidents: bool, dedup_lookback_days: int) -> bool:
    if dedup_lookback_days < 0:
        raise ValueError("dedup_lookback_days must be greater than or equal to 0.")

    return skip_updated_incidents and dedup_lookback_days > 0


def _call_without_common_server_python_lb_debug_logs(func: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
    demisto_module = cast(Any, demisto)
    original_debug: Callable[..., Any] = demisto.debug

    def _filtered_debug(*debug_args: Any, **debug_kwargs: Any) -> Any:
        first_arg = debug_args[0] if debug_args else None
        if isinstance(first_arg, str) and first_arg.startswith("lb:"):
            return None
        return original_debug(*debug_args, **debug_kwargs)

    demisto_module.debug = _filtered_debug
    try:
        return func(*args, **kwargs)
    finally:
        demisto_module.debug = original_debug


def _filter_duplicate_fetch_incidents(raw_incidents: list[dict], last_run_state: dict) -> list[dict]:
    if not raw_incidents:
        return raw_incidents

    return cast(
        list[dict],
        _call_without_common_server_python_lb_debug_logs(
            filter_incidents_by_duplicates_and_limit,
            incidents_res=raw_incidents,
            last_run=last_run_state,
            fetch_limit=len(raw_incidents),
            id_field="id",
        ),
    )


def _prune_seen_incident_ids(
    seen_ids: dict[str, float],
    retention_seconds: int,
    *,
    now: float | None = None,
) -> dict[str, float]:
    """
    Drop every cached incident ID whose addition timestamp is older than
    `retention_seconds`.

    The function is pure (no I/O, no demisto calls) and accepts an explicit
    `now` so it stays trivially testable. It returns a NEW dict and never
    mutates the input.

    Contract:
        * `retention_seconds <= 0` -> the entire cache is dropped.
        * Entries with non-numeric / negative timestamps are treated as
          "unknown age" and dropped defensively (forward compatible against
          any future cache schema corruption).

    Trade-off vs `CommonServerPython.remove_old_incidents_ids`:
        We deliberately do NOT keep "the latest ID forever" - that creates
        unbounded cache growth when a single ID is never re-fetched and
        breaks the 1:1 contract with `dedup_lookback_days`.
    """
    if retention_seconds <= 0:
        return {}

    current_time = time.time() if now is None else now
    threshold = current_time - retention_seconds

    pruned: dict[str, float] = {}
    for inc_id, addition_time in seen_ids.items():
        if not isinstance(addition_time, int | float) or addition_time < 0:
            continue
        if addition_time >= threshold:
            pruned[inc_id] = float(addition_time)
    return pruned


def _update_fetch_seen_incident_ids_cache(
    last_run_state: dict,
    incidents: list[dict],
    dedup_lookback_days: int,
) -> None:
    """
    Append the IDs of `incidents` to the deduplication cache and prune
    entries older than `dedup_lookback_days`.

    The cache layout (`dict[str, float]`, id -> unix seconds) is intentionally
    identical to the format consumed by
    `CommonServerPython.filter_incidents_by_duplicates_and_limit`, so that:
        * forward compatibility holds (XSOAR helpers still read the cache),
        * backward compatibility holds (legacy state from earlier versions
          is read transparently).

    Mutates `last_run_state[LAST_RUN_SEEN_INCIDENT_IDS_KEY]` in place.
    """
    if not incidents:
        return

    raw_cache = last_run_state.get(LAST_RUN_SEEN_INCIDENT_IDS_KEY) or {}
    if not isinstance(raw_cache, dict):
        raw_cache = {}

    cache: dict[str, float] = {}
    for inc_id, addition_time in raw_cache.items():
        if isinstance(addition_time, int | float):
            cache[str(inc_id)] = float(addition_time)

    now_ts = time.time()
    for incident in incidents:
        inc_id = incident.get("id")
        if inc_id is None:
            continue
        cache[str(inc_id)] = now_ts

    retention_seconds = _convert_dedup_lookback_days_to_seconds(dedup_lookback_days)
    last_run_state[LAST_RUN_SEEN_INCIDENT_IDS_KEY] = _prune_seen_incident_ids(
        cache,
        retention_seconds=retention_seconds,
        now=now_ts,
    )


def _summarize_fetch_last_run_state(last_run_state: dict | None) -> dict[str, Any]:
    if not isinstance(last_run_state, dict):
        return {"last_fetch": {}, "seen_incident_ids_cache_size": 0}

    last_fetch = last_run_state.get("last_fetch", {})
    if not isinstance(last_fetch, dict):
        last_fetch = {}

    seen_incident_ids_cache = last_run_state.get(LAST_RUN_SEEN_INCIDENT_IDS_KEY, {})
    if not isinstance(seen_incident_ids_cache, dict):
        seen_incident_ids_cache = {}

    return {
        "last_fetch": last_fetch,
        "seen_incident_ids_cache_size": len(seen_incident_ids_cache),
    }


def test_module(client: Client) -> str:
    """
    Returning 'ok' indicates that the integration works like it is supposed to. Connection to the service is successful.

    :param client: GIB_TI client
    :return: 'ok' if test passed, anything else will fail the test.
    """
    if not client.get_available_collections_cached():
        return "There are no collections available"
    return "ok"


def _validate_incident_collections(client: Client, incident_collections: list[str]) -> None:
    """Validate that requested collections are well-formed and granted to the API user.

    Skips the network round-trip to ``/user/granted_collections`` when the
    caller passes an empty list: nothing to validate, and dialing out would
    only add a side-effect (and noise in tests that legitimately pass an
    empty selection).
    """
    if not incident_collections:
        return

    for collection_name in incident_collections:
        CommonHelpers.validate_collections(collection_name)

    available = client.get_available_collections_cached()
    unknown = [c for c in incident_collections if c not in available]
    if unknown:
        raise DemistoException(
            f"The following collections are not available for the current credentials: {', '.join(unknown)}. "
            "Either remove them from instance settings or request access from Group-IB."
        )


def fetch_incidents_command(
    client: Client,
    last_run: dict,
    first_fetch_time: str,
    incident_collections: list[str],
    max_requests: int,
    hunting_rules: int,
    combolist: bool = False,
    unique: bool = False,
    enable_probable_corporate_access: bool = False,
    skip_updated_incidents: bool = False,
    dedup_lookback_days: int = DEFAULT_DEDUP_LOOKBACK_DAYS,
) -> tuple[dict, list]:
    """
    This function will execute each interval (default is 1 minute).

    :param client: GIB_TI_Feed client.
    :param last_run: the greatest sequpdate we fetched from last fetch.
    :param first_fetch_time: if last_run is None then fetch all incidents since first_fetch_time.
    :param incident_collections: list of collections enabled by client.
    :param max_requests: count of requests to API per collection.
    :param hunting_rules: enable this parameter to collect using hunting rules

    :return: next_run will be last_run in the next fetch-incidents; incidents and indicators will be created in Demisto.
    """
    demisto.debug(
        "[fetch-incidents] Starting fetch with params: "
        f"collections={incident_collections}, max_requests={max_requests}, "
        f"hunting_rules={hunting_rules}, combolist={combolist}, unique={unique}, "
        f"enable_probable_corporate_access={enable_probable_corporate_access}, "
        f"first_fetch_time={first_fetch_time}, skip_updated_incidents={skip_updated_incidents}, "
        f"dedup_lookback_days={dedup_lookback_days}"
    )
    dedup_enabled = _is_fetch_dedup_enabled(
        skip_updated_incidents=skip_updated_incidents,
        dedup_lookback_days=dedup_lookback_days,
    )
    incidents: list[dict] = []
    last_run_state = last_run.copy() if isinstance(last_run, dict) else {}
    demisto.debug(f"[fetch-incidents] Initial last_run summary: {_summarize_fetch_last_run_state(last_run_state)}")
    next_run: dict[str, dict[str, int | Any]] = {"last_fetch": {}}
    _validate_incident_collections(client=client, incident_collections=incident_collections)
    for collection_name in incident_collections:
        last_fetch_raw = None
        if isinstance(last_run_state, dict):
            embedded = last_run_state.get("last_fetch")
            if isinstance(embedded, dict):
                last_fetch_raw = embedded.get(collection_name)
            else:
                last_fetch_raw = last_run_state.get(collection_name)
        demisto.debug(f"[fetch-incidents] Collection={collection_name} previous_last_fetch={last_fetch_raw}")
        requests_count = 0
        sequpdate = 0

        if collection_name == COLLECTION_COMPROMISED_BREACHED_DB:
            last_fetch_for_generator = last_fetch_raw
        else:
            last_fetch_int = _parse_seq_update(last_fetch_raw)
            last_fetch_for_generator = (
                _serialize_seq_update(last_fetch_int) if isinstance(last_fetch_int, int) and last_fetch_int > 0 else None
            )

        portions, generator_cursor_raw = client.create_poll_generator(
            collection_name=collection_name,
            hunting_rules=hunting_rules,
            last_fetch=last_fetch_for_generator,
            first_fetch_time=first_fetch_time,
            enable_probable_corporate_access=enable_probable_corporate_access,
            combolist=combolist,
            unique=unique,
        )

        mapping = MAPPING.get(collection_name, {})
        demisto.debug(f"[fetch-incidents] Collection={collection_name} generator created: {portions}")

        generator_cursor_int = (
            None if collection_name == COLLECTION_COMPROMISED_BREACHED_DB else _parse_seq_update(generator_cursor_raw)
        )
        max_seen_seq_update: int | None = None

        for portion in portions:
            sequpdate = portion.sequpdate
            demisto.debug(
                f"[fetch-incidents] Portion received: collection={collection_name}, seqUpdate={sequpdate}, "
                f"portion_size={portion.portion_size}, count={portion.count}"
            )
            new_parsed_json = portion.bulk_parse_portion(keys_list=[mapping], as_json=False)
            if not isinstance(new_parsed_json, list):
                raise Exception("new_parsed_json in portion should be a list")

            portion_seq_int = _parse_seq_update(sequpdate)
            if (
                isinstance(generator_cursor_int, int)
                and isinstance(portion_seq_int, int)
                and portion_seq_int <= generator_cursor_int
            ):
                demisto.debug(
                    f"[fetch-incidents] seqUpdate did not advance (portion_seq={sequpdate}, "
                    f"cursor_seq={generator_cursor_raw}); skipping portion to avoid duplicates."
                )
                break

            if new_parsed_json and isinstance(new_parsed_json[0], list):
                iterable: Iterable[dict] = cast(Iterable[dict], chain.from_iterable(new_parsed_json))
            else:
                iterable = cast(Iterable[dict], new_parsed_json)

            raw_incidents = [item for item in iterable if isinstance(item, dict) and item.get("id")]
            if not raw_incidents:
                demisto.debug(
                    f"[fetch-incidents] Portion contains no incidents with ids; skipping addition. "
                    f"collection={collection_name}, seqUpdate={sequpdate}"
                )
                continue

            filtered_incidents = (
                _filter_duplicate_fetch_incidents(raw_incidents=raw_incidents, last_run_state=last_run_state)
                if dedup_enabled
                else raw_incidents
            )
            skipped_duplicates = len(raw_incidents) - len(filtered_incidents)
            if skipped_duplicates > 0:
                demisto.debug(
                    f"[fetch-incidents] Dedup skipped {skipped_duplicates} already-seen incidents "
                    f"for collection={collection_name}, seqUpdate={sequpdate}"
                )

            before_count = len(incidents)
            incidents.extend(
                IncidentBuilder(
                    collection_name=collection_name,
                    incident=incident,
                    mapping=mapping,
                ).build_incident()
                for incident in filtered_incidents
            )
            added = len(incidents) - before_count
            demisto.debug(f"[fetch-incidents] Built incidents for portion: added={added}, total={len(incidents)}")
            if dedup_enabled and filtered_incidents:
                _update_fetch_seen_incident_ids_cache(
                    last_run_state=last_run_state,
                    incidents=filtered_incidents,
                    dedup_lookback_days=dedup_lookback_days,
                )

            if isinstance(portion_seq_int, int) and portion_seq_int > 0:
                max_seen_seq_update = (
                    portion_seq_int if max_seen_seq_update is None else max(max_seen_seq_update, portion_seq_int)
                )

            requests_count += 1
            if requests_count >= max_requests:
                break

        if collection_name == COLLECTION_COMPROMISED_BREACHED_DB:
            next_run["last_fetch"][collection_name] = generator_cursor_raw if isinstance(generator_cursor_raw, dict) else None
        else:
            demisto.debug(f"[fetch-incidents] Final seqUpdate for collection={collection_name}: {sequpdate}")
            effective_last_fetch_int: int | None = None
            if isinstance(max_seen_seq_update, int) and max_seen_seq_update > 0:
                effective_last_fetch_int = max_seen_seq_update
            elif isinstance(generator_cursor_int, int) and generator_cursor_int > 0:
                effective_last_fetch_int = generator_cursor_int

            next_run["last_fetch"][collection_name] = (
                _serialize_seq_update(effective_last_fetch_int)
                if isinstance(effective_last_fetch_int, int) and effective_last_fetch_int > 0
                else None
            )
        demisto.debug(
            f"[fetch-incidents] Updated next_run for collection={collection_name}: " f"{next_run['last_fetch'][collection_name]}"
        )

    next_run[LAST_RUN_SEEN_INCIDENT_IDS_KEY] = last_run_state.get(LAST_RUN_SEEN_INCIDENT_IDS_KEY, {})
    demisto.debug(f"[fetch-incidents] Final next_run summary: {_summarize_fetch_last_run_state(next_run)}")
    return next_run, incidents


def get_available_collections_command(client: Client, args: dict | None = None):
    """
    Returns list of available collections to context and War Room.

    :param client: GIB_TI_Feed client.
    """

    my_collections = client.get_available_collections_proxy_function()
    readable_output = tableToMarkdown(
        name="Available collections",
        t={"collections": my_collections},
        headers="collections",
    )
    return CommandResults(
        outputs_prefix="GIBTI.OtherInfo",
        outputs_key_field="collections",
        outputs={"collections": my_collections},
        readable_output=readable_output,
        ignore_auto_extract=True,
        raw_response=my_collections,
    )


def get_info_by_id_command(collection_name: str):
    """
    Decorator around actual commands, that returns command depends on `collection_name`.
    """

    def get_info_by_id_for_collection(client: Client, args: dict) -> list[CommandResults]:
        """
        This function returns additional information to context and War Room.

        :param client: GIB_TI_Feed client.
        :param args: arguments, provided by client.
        """
        results = []
        CommonHelpers.validate_collections(collection_name)
        feed, main_table_data, additional_tables, indicators, readable_output = BuilderCommandResponses(
            client=client, collection_name=collection_name, args=args
        ).build_feed()

        results.append(
            CommandResults(
                outputs_prefix="GIBTI.{}".format(PREFIXES.get(collection_name, "").replace(" ", "")),
                outputs_key_field="id",
                outputs=feed,
                readable_output=readable_output,
                raw_response=feed,
                ignore_auto_extract=True,
            )
        )
        results.extend(additional_tables)
        results.extend(indicators)
        return results

    return get_info_by_id_for_collection


def global_search_command(client: Client, args: dict) -> CommandResults:
    query = str(args.get("query"))
    # `search_proxy_function` (and `global_search` under the hood) may
    # legitimately return null or an empty list when Group-IB has no
    # data on the query. Treat both as "no results" instead of letting
    # `for result in raw_response` raise TypeError.
    raw_response = client.search_proxy_function(query=query)
    handled_list: list[dict[str, Any]] = []
    for result in raw_response or []:
        if not isinstance(result, dict):
            continue
        if result.get("apiPath") in MAPPING:
            apiPath = result.get("apiPath")
            handled_list.append(
                {
                    "apiPath": apiPath,
                    "count": result.get("count"),
                    "GIBLink": result.get("link"),
                    "query": f"{apiPath}?q={query}",
                }
            )
    if len(handled_list) != 0:
        results = CommandResults(
            outputs_prefix="GIBTI.search.global",
            outputs_key_field="query",
            outputs=handled_list,
            readable_output=tableToMarkdown(
                "Search results",
                t=handled_list,
                headers=["apiPath", "count", "GIBLink"],
                url_keys=["GIBLink"],
            ),
            raw_response=raw_response,
            ignore_auto_extract=True,
        )
    else:
        results = CommandResults(
            outputs_prefix="GIBTI.search.global",
            raw_response=raw_response,
            ignore_auto_extract=True,
            outputs=[],
            readable_output="No results found for your query.",
        )
    return results


def local_search_command(client: Client, args: dict) -> CommandResults:
    def _parse_optional_int(value: Any, arg_name: str) -> int | None:
        if value is None:
            return None
        if isinstance(value, int):
            return value
        if isinstance(value, str):
            stripped = value.strip()
            if not stripped:
                return None
            try:
                return int(stripped)
            except ValueError as e:
                raise DemistoException(f"Invalid '{arg_name}' value: expected int, got {value!r}") from e
        raise DemistoException(f"Invalid '{arg_name}' type: expected int/str, got {type(value).__name__}")

    query = args.get("query")
    collection_name = str(args.get("collection_name"))
    include_raw_feed = argToBoolean(args.get("include_raw_feed", False))

    CommonHelpers.validate_collections(collection_name)

    requests_limit = _parse_optional_int(args.get("requests_limit"), "requests_limit") or 1
    page_size_limit = _parse_optional_int(args.get("page_size_limit"), "page_size_limit")
    filter_seq_update = _parse_optional_int(args.get("seq_update"), "seq_update")

    demisto.debug(
        "[local_search] Params: "
        f"collection={collection_name}, query={query!r}, "
        f"seq_update={filter_seq_update!r}, requests_limit={requests_limit}, page_size_limit={page_size_limit}, "
        f"include_raw_feed={include_raw_feed}"
    )

    update_kwargs: dict[str, Any] = {
        "collection_name": collection_name,
        "query": query,
        "limit": page_size_limit,
    }
    if filter_seq_update is not None:
        update_kwargs["sequpdate"] = filter_seq_update

    portions = client.poller.create_update_generator(**update_kwargs)

    mapping = MAPPING.get(collection_name, {})

    requests_count = 0
    result_list: list[dict[str, Any]] = []
    for portion in portions or []:
        if portion is None:
            continue
        sequpdate = getattr(portion, "sequpdate", None)
        new_parsed_json = portion.parse_portion(keys=mapping, as_json=False) or []
        for feed in new_parsed_json:
            if not isinstance(feed, dict):
                continue
            name = feed.get("name")
            additional_info = f"Name: {name}" if name else None
            entry: dict[str, Any] = {
                "id": feed.get("id"),
                "additional_info": additional_info,
                "seqUpdate": sequpdate,
            }
            if include_raw_feed:
                entry["raw_feed"] = CommonHelpers.safe_json_one_line(feed)
            result_list.append(entry)
        requests_count += 1
        if requests_limit is not None and requests_count >= requests_limit:
            break

    return CommandResults(
        outputs_prefix="GIBTI.search.local",
        outputs_key_field="id",
        outputs=result_list,
        readable_output=tableToMarkdown(
            "Search results",
            t=result_list,
            headers=["id", "additional_info", "seqUpdate", "raw_feed"],
        ),
        ignore_auto_extract=True,
    )


class ReputationCommandProcessor:
    ALLOWED_PATHS: dict[str, list[str]] = {
        "file": ["ioc/common"],
        "domain": [
            "apt/threat",
            "apt/threat_actor",
            "attacks/deface",
            "hi/open_threats",
            "ioc/common",
        ],
        # "scoring",
        "ip": [
            "apt/threat",
            "apt/threat_actor",
            "attacks/deface",
            "hi/open_threats",
            "ioc/common",
        ],
    }
    SENSITIVE_TO_DATES_COLLECTIONS: dict[str, list[str]] = {
        "domain": [
            "attacks/deface",
            "hi/open_threats",
            "ioc/common",
        ],
    }
    DATES_MAPPING: dict[str, dict[str, dict[str, str]]] = {
        "domain": {
            "attacks/deface": {
                "date": "date",
            },
            "hi/open_threats": {
                "detected": "detected",
            },
            "ioc/common": {"dateLastSeen": "dateLastSeen"},
        }
    }
    RECENT_WINDOW = timedelta(days=365 * 3)
    DATE_FORMATS = ("%Y-%m-%d", "%Y-%m-%dT%H:%M:%SZ")
    RELIABILITY_BY_COLLECTION: dict[str, dict[str, Reliability]] = {
        "file": {
            "ioc/common": DBotScoreReliability.A,
        },
        "domain": {
            "apt/threat": DBotScoreReliability.A,
            "apt/threat_actor": DBotScoreReliability.A,
            "ioc/common": DBotScoreReliability.A,
            "attacks/deface": DBotScoreReliability.B,
            "hi/open_threats": DBotScoreReliability.B,
        },
        "ip": {
            "apt/threat": DBotScoreReliability.A,
            "apt/threat_actor": DBotScoreReliability.A,
            "ioc/common": DBotScoreReliability.A,
            "attacks/deface": DBotScoreReliability.B,
            "hi/open_threats": DBotScoreReliability.B,
        },
    }
    RULES: list[dict[str, Any]] = [
        # IOC common last 3 years -> BAD
        {"any_recent": [("ioc/common", "dateLastSeen")], "score": Common.DBotScore.BAD},
        # open threats / defaces last 3 years -> SUSPICIOUS
        {
            "any_recent": [("hi/open_threats", "detected"), ("attacks/deface", "date")],
            "score": Common.DBotScore.SUSPICIOUS,
        },
        # IOC Common >3 years or no date -> SUSPICIOUS (if records exist)
        {
            "ioc_stale_or_no_date": ("ioc/common", "dateLastSeen"),
            "score": Common.DBotScore.SUSPICIOUS,
        },
        # no findings -> NONE
        {"no_findings": True, "score": Common.DBotScore.NONE},
    ]

    GRAPH_MAPPING = {
        "ip": {
            "asn": "whoisSummary.asn",
            "country": "whoisSummary.country",
            "descr": "whoisSummary.descr",
            "isp": "whoisSummary.isp",
            "netname": "whoisSummary.netname",
            "phone": "whoisSummary.phone",
        }
    }

    def __init__(
        self,
        client: Client,
        args: dict,
        integration_reliability: Reliability | None = None,
    ) -> None:
        self.client = client
        self.args = args
        self.integration_reliability = integration_reliability

    def _extract_indicator(self, indicator_name: str, arg_keys: list[str]) -> str:
        for key in arg_keys:
            value = self.args.get(key)
            if value:
                return str(value)
        raise DemistoException(f"Argument '{indicator_name}' is required.")

    def _filter_allowed_paths(self, indicator_name: str, exclude: list[str]) -> list[str]:
        base_paths = self.ALLOWED_PATHS.get(indicator_name, [])
        if not exclude:
            return base_paths
        exclude_set = set(exclude)
        return [p for p in base_paths if p not in exclude_set]

    def _get_indicator_data(self, indicator_name: str, indicator_value: str, search_data: list) -> dict:
        data_per_collections: dict[str, Any] = {}
        allowed_paths = self.ALLOWED_PATHS.get(indicator_name, [])
        for path, _count in search_data or []:
            if path not in allowed_paths:
                continue
            try:
                portions_data = IndicatorsHelper.collect_portions_for_indicator(
                    indicator_name=indicator_name,
                    indicator_value=indicator_value,
                    path=path,
                    poller=self.client.poller,
                    dates_mapping=self.DATES_MAPPING.get(indicator_name),
                    sensitive_collections=self.SENSITIVE_TO_DATES_COLLECTIONS.get(indicator_name, []),
                )
            except Exception as exc:
                # A 200/null or generator-yields-None response from any one
                # collection must not abort the whole indicator lookup. The
                # collection is treated as "no data" and the other
                # collections continue to be queried.
                demisto.debug(f"[reputation] collect_portions_for_indicator({path!r}, {indicator_value!r}) failed: {exc}")
                portions_data = []
            data_per_collections[path] = portions_data or []

        if indicator_name == "ip":
            try:
                ip_data = IndicatorsHelper.build_ip_enrichment(
                    poller=self.client.poller,
                    indicator_value=indicator_value,
                    mapping=self.GRAPH_MAPPING.get(indicator_name, {}),
                )
            except Exception as exc:
                demisto.debug(f"[reputation] build_ip_enrichment failed for {indicator_value!r}: {exc}")
                ip_data = {}
            data_per_collections.update(ip_data or {})
        return data_per_collections

    def _get_search_data(self, indicator_value):
        """Return [(apiPath, count), ...] for the indicator, or [] if no data.

        The Group-IB `global_search` endpoint legitimately returns HTTP 200
        with a `null` body when nothing is known about an indicator (and in
        rare schema-drift cases a dict instead of a list). Both shapes must
        produce an empty result — never a TypeError — so the reputation
        command can still return a clean DBotScore.NONE response.
        """
        try:
            search = self.client.poller.global_search(indicator_value)
        except Exception as exc:
            demisto.debug(f"[reputation] global_search failed for {indicator_value!r}: {exc}")
            return []
        if search in (None, "", [], {}):
            demisto.debug(f"[reputation] global_search returned no data for {indicator_value!r}")
            return []
        if not isinstance(search, list):
            demisto.debug(
                f"[reputation] global_search returned non-list ({type(search).__name__}) "
                f"for {indicator_value!r}; treating as no data"
            )
            return []
        finding: list[tuple[Any, Any]] = []
        for found in search:
            if not isinstance(found, dict):
                continue
            finding.append((found.get("apiPath"), found.get("count")))
        return finding

    def _parse_date(self, s):
        if not s:
            return None
        for fmt in self.DATE_FORMATS:
            try:
                return datetime.strptime(s, fmt)
            except ValueError:
                pass
        return None

    def _any_recent(self, items, date_key, now):
        for it in items or []:
            dt = self._parse_date((it or {}).get(date_key))
            if dt and (now - dt) <= self.RECENT_WINDOW:
                return True
        return False

    def _any_present(self, items, date_key):
        return any((it or {}).get(date_key) for it in (items or []))

    def _get_score(self, indicator_name, indicator_data):
        if indicator_name == "file":
            # if at least one element is found in ioc/common -> BAD, otherwise NONE
            score = Common.DBotScore.BAD if indicator_data.get("ioc/common") else Common.DBotScore.NONE

        elif indicator_name == "domain":
            # Rules:
            # - IOC common last 3 years -> BAD
            # - open threats / defaces last 3 years -> SUSPICIOUS
            # - IOC Common > 3 years or no date -> SUSPICIOUS (if there are records)
            # - no findings -> NONE
            now = datetime.utcnow()
            score = None

            for rule in self.RULES:
                any_recent = rule.get("any_recent")
                if any_recent and any(self._any_recent(indicator_data.get(coll), key, now) for coll, key in any_recent):
                    score = rule["score"]
                    break

                if rule.get("ioc_stale_or_no_date"):
                    coll, key = rule["ioc_stale_or_no_date"]
                    items = indicator_data.get(coll) or []
                    if items and (not self._any_present(items, key) or not self._any_recent(items, key, now)):
                        score = rule["score"]
                        break

                if rule.get("no_findings"):
                    has_any = any(indicator_data.get(c) for c in ("attacks/deface", "hi/open_threats", "ioc/common"))
                    if not has_any:
                        score = rule["score"]
                        break

            if score is None:
                score = Common.DBotScore.NONE

        elif indicator_name == "ip":
            # riskScore mapping to DBotScore:
            # 0-49 -> GOOD, 50-84 -> SUSPICIOUS, 85-100 -> BAD, None/out-of-range -> NONE
            scoring = indicator_data.get("scoring", {}).get("score")
            score = IndicatorsHelper.dbot_from_score(scoring)

        else:
            score = Common.DBotScore.NONE

        return score

    @staticmethod
    def _pick_best_reliability(reliabilities: list[Reliability]) -> Reliability | None:
        """
        Pick the most trusted reliability deterministically.

        Current policy:
        - Prefer A over B
        - Otherwise None
        """
        # Use a set to avoid order-dependence and make membership checks explicit.
        rset = set(reliabilities)
        if DBotScoreReliability.A in rset:
            return DBotScoreReliability.A
        if DBotScoreReliability.B in rset:
            return DBotScoreReliability.B
        return None

    def _get_reliability(self, indicator_name: str, indicator_data: dict[str, Any]) -> Reliability | None:
        if self.integration_reliability:
            return self.integration_reliability
        if indicator_name == "file":
            # if found in ioc/common, always A - Completely reliable : ‘a’:DBotScoreReliability.A
            reliability = DBotScoreReliability.A if indicator_data.get("ioc/common") else None
        elif indicator_name == "domain":
            # Summary:
            # - A: any match in apt/* or ioc/common
            # - B: any match in attacks/deface or hi/open_threats
            #
            # Detailed mapping:
            # - nation state (apt/threat, apt/threat_actor) -> A - Completely reliable
            # - other IOC common (ioc/common) -> A - Completely reliable
            # - defaces (attacks/deface) -> B - Usually reliable
            # - open threats (hi/open_threats) -> B - Usually reliable
            matched_reliabilities = [
                self.RELIABILITY_BY_COLLECTION.get(indicator_name, {}).get(coll)
                for coll in self.ALLOWED_PATHS.get(indicator_name, [])
                if indicator_data.get(coll)
            ]
            reliability = self._pick_best_reliability([r for r in matched_reliabilities if r])
        elif indicator_name == "ip":
            # Summary:
            # - A: any match in apt/* or ioc/common
            # - B: any match in attacks/deface or hi/open_threats
            #
            # Detailed mapping:
            # - nation state (apt/threat, apt/threat_actor) -> A - Completely reliable
            # - other IOC common (ioc/common) -> A - Completely reliable
            # - defaces (attacks/deface) -> B - Usually reliable
            # - open threats (hi/open_threats) -> B - Usually reliable
            matched_reliabilities = [
                self.RELIABILITY_BY_COLLECTION.get(indicator_name, {}).get(coll)
                for coll in self.ALLOWED_PATHS.get(indicator_name, [])
                if indicator_data.get(coll)
            ]
            reliability = self._pick_best_reliability([r for r in matched_reliabilities if r])
        else:
            reliability = None
        return reliability

    def _normalize_graph_ip(self, graph_ip_info: Any) -> dict[str, Any]:
        """Normalize graph_ip response to a single dict."""
        if isinstance(graph_ip_info, list):
            if graph_ip_info:
                graph_ip_info = graph_ip_info[0] or {}
            else:
                graph_ip_info = {}
        if not isinstance(graph_ip_info, dict):
            return {}
        return graph_ip_info

    def _build_ip_enrichment_kwargs(self, graph_ip_info: dict[str, Any]) -> dict[str, Any]:
        """Build kwargs for Common.IP from graph_ip whois data."""
        return {
            "asn": graph_ip_info.get("asn"),
            "as_owner": graph_ip_info.get("isp"),
            "geo_country": graph_ip_info.get("country"),
            "geo_description": graph_ip_info.get("descr") or graph_ip_info.get("netname"),
            "registrar_abuse_phone": graph_ip_info.get("phone"),
            "organization_name": graph_ip_info.get("netname"),
            "description": graph_ip_info.get("descr") or graph_ip_info.get("netname"),
        }

    @staticmethod
    def _build_readable_output(
        title: str,
        indicator_value: str,
        score_value: Any,
        reliability: Any = None,
        numerical_score: Any = None,
    ) -> str:
        table_data = {
            "Indicator": indicator_value,
            "Score": {v: k for k, v in COMMON_SCORE_MAP.items()}.get(score_value, score_value),
        }
        if reliability is not None:
            table_data["Reliability"] = reliability
        if numerical_score is not None:
            table_data["Numerical Score"] = numerical_score

        return tableToMarkdown(
            title,
            table_data,
            removeNull=True,
        )

    def run(
        self,
        indicator_name: str,
        indicator_type,
        arg_keys: list[str] | None = None,
    ) -> CommandResults:
        arg_keys = arg_keys or ["value", indicator_name]
        indicator_value = self._extract_indicator(indicator_name, arg_keys)
        search_data = self._get_search_data(indicator_value=indicator_value)
        indicator_data = self._get_indicator_data(indicator_name, indicator_value, search_data)
        score = self._get_score(indicator_name, indicator_data)
        reliability = self._get_reliability(indicator_name, indicator_data)
        graph_ip_info = indicator_data.get("graph_ip") or {}
        graph_ip_info = self._normalize_graph_ip(graph_ip_info)

        d_bot_score = Common.DBotScore(
            indicator=indicator_value,
            indicator_type=indicator_type,
            integration_name="GroupIBTI",
            score=score,
            reliability=reliability,
        )
        indicator_obj: Any = None
        if indicator_name == "ip":
            indicator_obj = Common.IP(
                ip=indicator_value,
                dbot_score=d_bot_score,
                **self._build_ip_enrichment_kwargs(graph_ip_info),
            )
        elif indicator_name == "domain":
            indicator_obj = Common.Domain(domain=indicator_value, dbot_score=d_bot_score)
        elif indicator_name == "file":
            # hash type is not specified; pass as md5 for DBot correlation
            indicator_obj = Common.File(md5=indicator_value, dbot_score=d_bot_score)

        # When Group-IB has no data across every collection (HTTP 200 / null
        # body), prepend a one-line note to the War-Room readable so the
        # analyst sees "no data" instead of a bare "Unknown" row. The
        # context contract (`raw_response` fields) is unchanged.
        has_payload = any(isinstance(v, list) and v for k, v in indicator_data.items() if k not in ("scoring", "graph_ip"))
        base_readable = self._build_readable_output(
            title=f"Group-IB reputation for {indicator_value}",
            indicator_value=indicator_value,
            score_value=score,
            reliability=reliability,
        )
        if not has_payload:
            readable_output = f"No Group-IB Threat Intelligence data was found for `{indicator_value}`.\n\n" + base_readable
        else:
            readable_output = base_readable

        return CommandResults(
            readable_output=readable_output,
            indicator=indicator_obj,
            raw_response={
                "indicator": indicator_value,
                "score": score,
                "reliability": str(reliability),
            },
        )


def gibti_ip_scoring_command(client: Client, args: dict) -> CommandResults:
    indicator_value = args.get("ip")
    if not indicator_value:
        raise DemistoException("Argument 'ip' is required.")

    ip_data = IndicatorsHelper.build_ip_enrichment(
        poller=client.poller,
        indicator_value=indicator_value,
        mapping={},
    )
    risk_score = ip_data.get("scoring", {}).get("score")
    dbot_score_value = IndicatorsHelper.dbot_from_score(risk_score)

    d_bot_score = Common.DBotScore(
        indicator=indicator_value,
        indicator_type=DBotScoreType.IP,
        integration_name="GroupIBTI",
        score=dbot_score_value,
        reliability=None,
    )

    indicator_obj = Common.IP(
        ip=indicator_value,
        dbot_score=d_bot_score,
    )

    readable_output = ReputationCommandProcessor._build_readable_output(
        title=f"Group-IB scoring for {indicator_value}",
        indicator_value=indicator_value,
        score_value=dbot_score_value,
        numerical_score=risk_score,
    )

    return CommandResults(
        readable_output=readable_output,
        indicator=indicator_obj,
        raw_response={
            "indicator": indicator_value,
            "score": dbot_score_value,
            "riskScore": risk_score,
        },
    )


class ReputationCommands:
    @staticmethod
    def file(
        client: Client,
        args: dict,
        integration_reliability: Reliability | None = None,
    ) -> CommandResults:
        return ReputationCommandProcessor(client, args, integration_reliability).run(
            indicator_name="file", indicator_type=DBotScoreType.FILE
        )

    @staticmethod
    def domain(
        client: Client,
        args: dict,
        integration_reliability: Reliability | None = None,
    ) -> CommandResults:
        return ReputationCommandProcessor(client, args, integration_reliability).run(
            indicator_name="domain", indicator_type=DBotScoreType.DOMAIN
        )

    @staticmethod
    def ip(
        client: Client,
        args: dict,
        integration_reliability: Reliability | None = None,
    ) -> CommandResults:
        return ReputationCommandProcessor(client, args, integration_reliability).run(
            indicator_name="ip", indicator_type=DBotScoreType.IP
        )


class ReputationCommandPolicy:
    _SUPPORTED_REPUTATION_COMMANDS: frozenset[str] = frozenset({"ip", "domain", "file"})

    def __init__(self, enabled_commands: set[str]) -> None:
        self._enabled_commands = enabled_commands

    @classmethod
    def from_params(cls, params: dict) -> "ReputationCommandPolicy":
        """
        Policy precedence:
        - Allow-list only: only explicitly enabled commands can run.
        - Fail-safe default: if the param is missing or empty -> no reputation commands run.
        """

        raw_enabled = params.get("enabled_reputation_commands") or []
        enabled = {str(x).strip().lower() for x in argToList(raw_enabled) if str(x).strip()}
        enabled &= set(cls._SUPPORTED_REPUTATION_COMMANDS)
        return cls(enabled_commands=enabled)

    def is_enabled(self, command: str) -> bool:
        return command in self._enabled_commands

    @staticmethod
    def build_not_enabled_result(command: str) -> CommandResults:
        return CommandResults(
            readable_output=(
                f"Reputation command '{command}' is not enabled in the integration instance settings. "
                "No enrichment was performed."
            ),
            raw_response={"command": command, "enabled": False},
        )


def main():
    """
    PARSE AND VALIDATE INTEGRATION PARAMS
    """
    incident_collections = None
    try:
        params = demisto.params()
        credentials: dict = params.get("credentials", {})
        username = credentials.get("identifier")
        password = credentials.get("password")
        base_url = str(params.get("url"))
        proxy = params.get("proxy", False)
        hunting_rules = params.get("hunting_rules", 0)
        verify_certificate = not params.get("insecure", False)
        endpoint = None
        result: Any = None

        incident_collections = params.get("incident_collections", [])
        incidents_first_fetch = params.get("first_fetch", "3 days").strip()
        requests_count = int(params.get("max_fetch", 3))
        skip_updated_incidents = argToBoolean(params.get("skip_updated_incidents", False))
        dedup_lookback_days = _get_dedup_lookback_days_from_params(params)

        combolist = params.get("combolist", False)
        unique = params.get("unique", False)
        enable_probable_corporate_access = params.get("enable_probable_corporate_access", False)
        limit_param = params.get("limit", 100)
        limit = int(limit_param)
        integration_reliability_param = params.get("integration_reliability")
        disable_reliability_override = params.get("disable_integration_reliability_override", False)
        integration_reliability = (
            None if disable_reliability_override else IndicatorsHelper.parse_source_reliability(integration_reliability_param)
        )
        reputation_policy = ReputationCommandPolicy.from_params(params)

        args = demisto.args()
        raw_command = demisto.command()
        command_aliases = {
            "gibtia-get-compromised-account-info": "gibti-get-compromised-account-info",
            "gibtia-get-compromised-card-group-info": "gibti-get-compromised-card-group-info",
            "gibtia-get-compromised-mule-info": "gibti-get-compromised-mule-info",
            "gibtia-get-compromised-spd-info": "gibti-get-compromised-spd-info",
            "gibtia-get-compromised-breached-info": "gibti-get-compromised-breached-info",
            "gibtia-get-phishing-kit-info": "gibti-get-phishing-kit-info",
            "gibtia-get-phishing-group-info": "gibti-get-phishing-group-info",
            "gibtia-get-osi-git-leak-info": "gibti-get-osi-git-leak-info",
            "gibtia-get-osi-public-leak-info": "gibti-get-osi-public-leak-info",
            "gibtia-get-osi-vulnerability-info": "gibti-get-osi-vulnerability-info",
            "gibtia-get-attacks-ddos-info": "gibti-get-attacks-ddos-info",
            "gibtia-get-attacks-deface-info": "gibti-get-attacks-deface-info",
            "gibtia-get-threat-info": "gibti-get-threat-info",
            "gibtia-get-threat-actor-info": "gibti-get-threat-actor-info",
            "gibtia-get-suspicious-ip-tor-node-info": "gibti-get-suspicious-ip-tor-node-info",
            "gibtia-get-suspicious-ip-open-proxy-info": "gibti-get-suspicious-ip-open-proxy-info",
            "gibtia-get-suspicious-ip-socks-proxy-info": "gibti-get-suspicious-ip-socks-proxy-info",
            "gibtia-get-suspicious-ip-vpn-info": "gibti-get-suspicious-ip-vpn-info",
            "gibtia-get-suspicious-ip-scanner-info": "gibti-get-suspicious-ip-scanner-info",
            "gibtia-get-malware-cnc-info": "gibti-get-malware-cnc-info",
            "gibtia-get-malware-malware-info": "gibti-get-malware-malware-info",
            "gibtia-get-available-collections": "gibti-get-available-collections",
            "gibtia-global-search": "gibti-global-search",
            "gibtia-local-search": "gibti-local-search",
        }
        command = command_aliases.get(raw_command, raw_command)
        demisto.debug(f"Command being called is {raw_command}, mapped to {command}")
        demisto.debug(
            "[main] Parsed params: "
            f"url={base_url}, proxy={proxy}, verify={verify_certificate}, "
            f"hunting_rules={hunting_rules}, first_fetch={incidents_first_fetch}, max_fetch={requests_count}, "
            f"collections={incident_collections}, combolist={combolist}, unique={unique}, "
            f"enable_probable_corporate_access={enable_probable_corporate_access}, limit={limit}"
        )

        client = Client(
            base_url=base_url,
            verify=verify_certificate,
            auth=(username, password),
            proxy=proxy,
            headers={"Accept": "*/*"},
            limit=limit,
        )
        demisto.info("Client created successfully")

        deprecated_commands = [
            "gibtia-get-compromised-card-info",
            "gibtia-get-compromised-imei-info",
            "gibtia-get-malware-targeted-malware-info",
            "gibtia-get-phishing-info",
        ]
        if raw_command in deprecated_commands or command in deprecated_commands:
            raise Exception(f"{command} deprecated")

        if hunting_rules is True:
            list_hunting_rules_collections = client.poller.get_hunting_rules_collections()

            for collection in incident_collections:
                if collection not in list_hunting_rules_collections:
                    raise Exception(f"Collection {collection} doesn't support hunting rules")
            hunting_rules = 1

        info_commands = {
            # new prefix
            "gibti-get-compromised-account-info": "compromised/account_group",
            "gibti-get-compromised-card-group-info": "compromised/bank_card_group",
            "gibti-get-compromised-masked-card-info": "compromised/masked_card",
            "gibti-get-compromised-mule-info": "compromised/mule",
            "gibti-get-compromised-spd-info": "compromised/spd",
            "gibti-get-compromised-breached-info": "compromised/breached",
            "gibti-get-phishing-kit-info": "attacks/phishing_kit",
            "gibti-get-phishing-group-info": "attacks/phishing_group",
            "gibti-get-osi-git-leak-info": "osi/git_repository",
            "gibti-get-osi-public-leak-info": "osi/public_leak",
            "gibti-get-osi-vulnerability-info": "osi/vulnerability",
            "gibti-get-attacks-ddos-info": "attacks/ddos",
            "gibti-get-attacks-deface-info": "attacks/deface",
            "gibti-get-threat-info": "threat",
            "gibti-get-threat-actor-info": "threat_actor",
            "gibti-get-suspicious-ip-tor-node-info": "suspicious_ip/tor_node",
            "gibti-get-suspicious-ip-open-proxy-info": "suspicious_ip/open_proxy",
            "gibti-get-suspicious-ip-socks-proxy-info": "suspicious_ip/socks_proxy",
            "gibti-get-suspicious-ip-vpn-info": "suspicious_ip/vpn",
            "gibti-get-suspicious-ip-scanner-info": "suspicious_ip/scanner",
            "gibti-get-malware-cnc-info": "malware/cnc",
            "gibti-get-malware-malware-info": "malware/malware",
        }

        other_commands = {
            # new prefix
            "gibti-get-available-collections": get_available_collections_command,
            "gibti-global-search": global_search_command,
            "gibti-local-search": local_search_command,
            "gibti-ip-scoring": gibti_ip_scoring_command,
        }
        reputation_commands = {
            "file": ReputationCommands.file,
            "domain": ReputationCommands.domain,
            "ip": ReputationCommands.ip,
        }

        if command == "test-module":
            # This is the call made when pressing the integration Test button.
            result = test_module(client)
            demisto.results(result)

        elif command == "fetch-incidents":
            # Set and define the fetch incidents command to run after activated via integration settings.
            last_run = demisto.getLastRun()
            next_run, incidents = fetch_incidents_command(
                client=client,
                last_run=last_run,
                first_fetch_time=incidents_first_fetch,
                incident_collections=incident_collections,
                max_requests=requests_count,
                hunting_rules=hunting_rules,
                combolist=combolist,
                unique=unique,
                enable_probable_corporate_access=enable_probable_corporate_access,
                skip_updated_incidents=skip_updated_incidents,
                dedup_lookback_days=dedup_lookback_days,
            )
            demisto.debug(f"[fetch-incidents] Incidents created this run: count={len(incidents)}")
            demisto.debug(f"[fetch-incidents] last_run summary before persist: {_summarize_fetch_last_run_state(last_run)}")
            demisto.debug(f"[fetch-incidents] next_run summary before persist: {_summarize_fetch_last_run_state(next_run)}")
            demisto.setLastRun(next_run)
            demisto.incidents(incidents)
        else:
            incident_collections = None
            if command in info_commands:
                endpoint = info_commands[command]
                result = get_info_by_id_command(endpoint)(client, args)
            elif command in reputation_commands:
                if not reputation_policy.is_enabled(command):
                    result = ReputationCommandPolicy.build_not_enabled_result(command)
                else:
                    result = reputation_commands[command](client, args, integration_reliability)  # type: ignore
            else:
                result = other_commands[command](client, args)  # type: ignore
            return_results(result)

    # Log exceptions
    except Exception:
        return_error(
            f"Failed to execute {demisto.command()} command.\n"
            f"Incident collection: {incident_collections}.\n"
            f"Command endpoint: {endpoint}.\n Error: {format_exc()}"
        )


if __name__ in ("__main__", "__builtin__", "builtins"):
    main()