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.

from unittest.mock import patch, MagicMock

import pytest

from CommonServerPython import CommandResults
from typing import Any, cast
from GroupIBTIA import (
    fetch_incidents_command,
    Client,
    get_available_collections_command,
    local_search_command,
    BuilderCommandResponses,
    CommonHelpers,
    INCIDENT_CREATED_DATES_MAPPING,
    IncidentBuilder,
    MAPPING,
    PORTAL_LINKS,
)
from urllib3.exceptions import InsecureRequestWarning
from urllib3 import disable_warnings as urllib3_disable_warnings
import GroupIBTIA
from json import load, loads
import os

realpath = os.path.join(os.path.dirname(os.path.realpath(__file__)))

with open(f"{realpath}/test_data/main_collections_examples.json") as example:
    COLLECTIONS_RAW_JSON = load(example)

with open(f"{realpath}/test_data/search_example.json") as example:
    SEARCH_RAW_JSON = load(example)

with open(f"{realpath}/test_data/avalible_collections_example.json") as example:
    AVALIBLE_COLLECTIONS_RAW_JSON = load(example)

# Disable insecure warnings
urllib3_disable_warnings(InsecureRequestWarning)

COLLECTION_NAMES = [
    "compromised/account_group",
    "compromised/bank_card_group",
    "compromised/mule",
    "osi/git_repository",
    "osi/vulnerability",
    "attacks/ddos",
    "attacks/deface",
    "attacks/phishing_group",
    "attacks/phishing_kit",
    "suspicious_ip/tor_node",
    "suspicious_ip/open_proxy",
    "suspicious_ip/socks_proxy",
    "suspicious_ip/vpn",
    "suspicious_ip/scanner",
    "malware/cnc",
    "hi/threat",
    "hi/threat_actor",
    "apt/threat",
    "apt/threat_actor",
    "malware/malware",
    "osi/public_leak",
    "compromised/breached",
]


@pytest.fixture(scope="function", params=COLLECTION_NAMES)
def session_fixture(request):
    """
    Fixture for creating a client instance specific to each collection name.

    Given:
      - A list of predefined collection names that represent different types of data.

    When:
      - Each test function requests an instance of this fixture.

    Then:
      - Returns a tuple with the current collection name and an instantiated Client object.
      - The Client instance is configured to interact with the appropriate collection by connecting
        to the integration's base URL, using authentication, and including necessary headers.
    """
    return request.param, Client(
        base_url="https://some-url.com",
        auth=("example@example.com", "exampleAPI_TOKEN"),
        verify=True,
        headers={"Accept": "*/*"},
    )


@pytest.fixture(scope="function")
def single_session_fixture():
    """
    Fixture for creating a generic client instance to be used across multiple tests.

    Given:
      - No specific parameters; only a need for a Client object with common configuration.

    When:
      - A test requires a general Client instance without needing to specify a collection.

    Then:
      - Returns a Client instance configured with the base URL, authentication, and headers.
      - The instance can be reused by any test that doesn't depend on a specific collection name.
    """
    return Client(
        base_url="https://some-url.com",
        auth=("example@example.com", "exampleAPI_TOKEN"),
        verify=True,
        headers={"Accept": "*/*"},
    )


def test_fetch_incidents(mocker, session_fixture):
    """
    Test for verifying the behavior of the fetch_incidents_command function.

    Given:
      - session_fixture, which provides a client instance associated with a specific collection name.
      - last_run, a dictionary representing the previous state of incident fetching.
      - first_fetch_time, a string specifying the starting time frame for incident retrieval.

    When:
      - fetch_incidents_command() is invoked with the above parameters.

    Then:
      - Ensures that the command returns the correct types for next_run and incidents.
      - Verifies that incidents is a list, as expected.
      - This test validates that the command correctly retrieves incidents for each collection
        and that the returned data structure matches the expected format.
    """
    collection_name, client = session_fixture
    collection_name, client = session_fixture
    mocker.patch.object(client, "create_poll_generator", return_value=[COLLECTIONS_RAW_JSON[collection_name]])
    next_run, incidents = fetch_incidents_command(
        client=client, last_run={}, first_fetch_time="3 days", incident_collections=[], max_requests=3, hunting_rules=False
    )
    assert isinstance(incidents, list)


def test_fetch_incidents_masked_card_collection(mocker, single_session_fixture):
    collection_name = "compromised/masked_card"
    client = single_session_fixture
    mock_portion = MagicMock()
    mock_portion.sequpdate = 1592219410029000
    mock_portion.portion_size = 1
    mock_portion.count = 1
    mock_portion.bulk_parse_portion.return_value = [
        {
            "id": "e66dbb9b2bdd55d5ecce174318060373f923c427",
            "name": "000000XXXXXXXXXX",
            "number": "000000XXXXXXXXXX",
            "issuer": None,
            "type": None,
            "payment_system": None,
            "validThru": "12/49",
            "address": None,
            "email": None,
            "owner_name": None,
            "phone": None,
            "dateDetected": "2020-05-22T17:04:25+00:00",
            "dateCompromised": "2020-05-15T09:17:45+00:00",
            "malware_name": "vendeta",
            "portalLink": "https://tap.group-ib.com/cd/cards?id=e66dbb9b2bdd55d5ecce174318060373f923c427",
            "evaluation": {
                "admiraltyCode": "A2",
                "credibility": 80,
                "reliability": 90,
                "severity": "red",
                "tlp": "red",
            },
            "sourceType": "Card shop",
            "threat_actor_id": None,
            "threat_actor_name": None,
            "threat_actor_is_apt": None,
            "indicators": {
                "cnc_url": None,
                "cnc_domain": "kingven.cc",
                "cnc_ipv4_ip": "11.11.11.11",
                "cnc_ipv4_asn": "AS63949",
                "cnc_ipv4_country_name": "United States",
                "cnc_ipv4_region": "North America",
            },
        }
    ]

    mocker.patch.object(client.poller, "get_available_collections", return_value=[collection_name])
    mocker.patch.object(client, "create_poll_generator", return_value=([mock_portion], None))

    next_run, incidents = fetch_incidents_command(
        client=client,
        last_run={},
        first_fetch_time="3 days",
        incident_collections=[collection_name],
        max_requests=3,
        hunting_rules=0,
    )

    assert len(incidents) == 1
    assert incidents[0]["dbotMirrorId"] == "e66dbb9b2bdd55d5ecce174318060373f923c427"
    assert next_run["last_fetch"][collection_name] == "1592219410029000"


def test_main_error():
    """
    Test for verifying the error-handling behavior in the main() function.

    Given:
      - A main() function configured to raise an exception when calling error_command.

    When:
      - The main function invokes error_command(), which is expected to trigger an error.

    Then:
      - Ensures that a SystemExit exception is raised as expected.
      - The test checks that the main function handles errors in a predictable and controlled
        manner, allowing graceful exits during failure.
    """
    with (
        pytest.raises(SystemExit),
        patch.object(
            GroupIBTIA.demisto,
            "params",
            return_value={
                "credentials": {"identifier": "user@example.com", "password": "token"},
                "url": "https://some-url.com",
                "proxy": False,
                "insecure": False,
                "incident_collections": [],
                "first_fetch": "3 days",
                "max_fetch": 1,
                "limit": 10,
            },
        ),
        patch.object(GroupIBTIA.demisto, "args", return_value={}),
        patch.object(GroupIBTIA.demisto, "command", return_value="non-existent-command"),
        patch.object(GroupIBTIA, "Client", autospec=True),
        patch.object(GroupIBTIA, "return_error", side_effect=SystemExit(1)),
    ):
        GroupIBTIA.main()


def test_reputation_command_disabled_ip(mocker):
    """
    Given: Integration instance configuration does not enable the 'ip' reputation command.
    When: The 'ip' command is invoked.
    Then: The integration must return a controlled no-op result and must not execute enrichment logic.
    """
    mocker.patch.object(
        GroupIBTIA.demisto,
        "params",
        return_value={
            "credentials": {"identifier": "user@example.com", "password": "token"},
            "url": "https://some-url.com",
            "proxy": False,
            "insecure": False,
            # New behavior: allow-list. Empty means disabled.
            "enabled_reputation_commands": [],
            "incident_collections": [],
            "first_fetch": "3 days",
            "max_fetch": 1,
            "limit": 10,
        },
    )
    mocker.patch.object(GroupIBTIA.demisto, "args", return_value={"ip": "8.8.8.8"})
    mocker.patch.object(GroupIBTIA.demisto, "command", return_value="ip")

    mocker.patch.object(GroupIBTIA, "Client", autospec=True)
    ip_impl = mocker.patch.object(GroupIBTIA.ReputationCommands, "ip", autospec=True)
    rr = mocker.patch.object(GroupIBTIA, "return_results", autospec=True)
    mocker.patch.object(GroupIBTIA, "return_error", side_effect=AssertionError("return_error must not be called"))

    GroupIBTIA.main()

    assert ip_impl.call_count == 0, "Expected ReputationCommands.ip not to be called when not enabled."
    assert rr.call_count == 1
    result_obj = rr.call_args[0][0]
    assert isinstance(result_obj, CommandResults)
    assert "not enabled" in (result_obj.readable_output or "").lower()


def test_reputation_commands_default_disabled_when_param_missing(mocker):
    """
    Given: Integration instance configuration does not include the 'enabled_reputation_commands' param.
    When: A reputation command (ip/domain/file) is invoked.
    Then: The integration must default to disabled (fail-safe) and must not execute enrichment logic.
    """
    mocker.patch.object(
        GroupIBTIA.demisto,
        "params",
        return_value={
            "credentials": {"identifier": "user@example.com", "password": "token"},
            "url": "https://some-url.com",
            "proxy": False,
            "insecure": False,
            "incident_collections": [],
            "first_fetch": "3 days",
            "max_fetch": 1,
            "limit": 10,
        },
    )
    mocker.patch.object(GroupIBTIA.demisto, "args", return_value={"ip": "8.8.8.8"})
    mocker.patch.object(GroupIBTIA.demisto, "command", return_value="ip")

    mocker.patch.object(GroupIBTIA, "Client", autospec=True)
    ip_impl = mocker.patch.object(GroupIBTIA.ReputationCommands, "ip", autospec=True)
    rr = mocker.patch.object(GroupIBTIA, "return_results", autospec=True)
    mocker.patch.object(GroupIBTIA, "return_error", side_effect=AssertionError("return_error must not be called"))

    GroupIBTIA.main()

    assert ip_impl.call_count == 0, "Expected ReputationCommands.ip not to be called when param is missing."
    assert rr.call_count == 1


def test_reputation_command_enabled_allow_list(mocker):
    """
    Given: Allow-list enables 'ip' reputation command.
    When: The 'ip' reputation command is invoked.
    Then: The integration must call the underlying reputation implementation.
    """
    mocker.patch.object(
        GroupIBTIA.demisto,
        "params",
        return_value={
            "credentials": {"identifier": "user@example.com", "password": "token"},
            "url": "https://some-url.com",
            "proxy": False,
            "insecure": False,
            "enabled_reputation_commands": ["ip"],
            "incident_collections": [],
            "first_fetch": "3 days",
            "max_fetch": 1,
            "limit": 10,
        },
    )
    mocker.patch.object(GroupIBTIA.demisto, "args", return_value={"ip": "8.8.8.8"})
    mocker.patch.object(GroupIBTIA.demisto, "command", return_value="ip")

    mocker.patch.object(GroupIBTIA, "Client", autospec=True)
    ip_impl = mocker.patch.object(
        GroupIBTIA.ReputationCommands,
        "ip",
        autospec=True,
        return_value=CommandResults(readable_output="ok"),
    )
    rr = mocker.patch.object(GroupIBTIA, "return_results", autospec=True)
    mocker.patch.object(GroupIBTIA, "return_error", side_effect=AssertionError("return_error must not be called"))

    GroupIBTIA.main()

    assert ip_impl.call_count == 1
    assert rr.call_count == 1


def test_global_search_command(mocker, single_session_fixture):
    """
    Test for verifying the functionality of the global_search_command function.

    Given:
      - single_session_fixture provides a client instance for performing a search.
      - A test_query dictionary with a "query" key specifying a search term, in this case, an IP address.

    When:
      - The global_search_command() function is called with the client and test_query arguments.

    Then:
      - Ensures that the command's outputs_prefix and outputs_key_field are correctly set to expected values.
      - Verifies that the command returns the data structure with the correct outputs_key_field ("query"),
        ensuring compatibility with other functions that depend on this structure.
      - This test validates that the search command integrates smoothly with the client and returns
        consistent output formatting.
    """
    client = single_session_fixture
    mocker.patch.object(client, "search_proxy_function", return_value=SEARCH_RAW_JSON)
    test_query = {"query": "8.8.8.8"}
    result = GroupIBTIA.global_search_command(client=client, args=test_query)

    assert result.outputs_prefix == "GIBTI.search.global"
    assert result.outputs_key_field == "query"


def test_get_available_collections(mocker, single_session_fixture):
    """
    Test for validating the get_available_collections_command function.

    Given:
      - single_session_fixture, which provides a client instance for retrieving available collections.

    When:
      - The get_available_collections_command() function is invoked with the client instance.

    Then:
      - Verifies that the outputs_prefix is correctly set to "GIBTI.OtherInfo", indicating that
        the response data is categorized as general information.
      - Checks that the outputs_key_field is "collections", matching the expected key for collections data.
      - Ensures that the "collections" field in the output contains a list of collection names, as expected.
      - This test confirms that the command accurately retrieves and formats the list of available
        collections from the server response.
    """
    client = single_session_fixture
    mocker.patch.object(client, "get_available_collections_proxy_function", return_value=[AVALIBLE_COLLECTIONS_RAW_JSON])
    result = get_available_collections_command(client=client)

    assert result.outputs_prefix == "GIBTI.OtherInfo"
    assert result.outputs_key_field == "collections"
    assert isinstance(result.outputs["collections"], list)


@pytest.fixture
def mock_client():
    """Fixture to create a mock client."""
    client = MagicMock()
    client.poller.create_update_generator.return_value = []
    return client


@pytest.fixture
def mock_common_helpers():
    """Fixture to mock CommonHelpers functions."""
    with (
        patch("GroupIBTIA.CommonHelpers.validate_collections") as mock_validate,
        patch("GroupIBTIA.CommonHelpers.date_parse") as mock_date_parse,
    ):
        mock_validate.return_value = None
        mock_date_parse.side_effect = lambda date, arg_name: f"parsed_{date}" if date else None
        yield mock_validate, mock_date_parse


def test_local_search_command_no_results(mock_client, mock_common_helpers):
    """
    Given: A valid collection name and search query, with no results returned by the client.
    When: The local_search_command function is executed.
    Then: The function should return an empty list with appropriate formatting.
    """
    args = {"query": "test_query", "collection_name": "test_collection"}

    result = local_search_command(mock_client, args)

    assert isinstance(result, CommandResults)
    assert result.outputs_prefix == "GIBTI.search.local"
    assert result.outputs_key_field == "id"
    assert result.outputs == []
    assert "Search results" in result.readable_output


def test_local_search_command_with_results(mock_client, mock_common_helpers):
    """
    Given: A valid collection name, search query, and results returned by the client.
    When: The local_search_command function is executed.
    Then: The function should return a formatted list of search results.
    """
    mock_client.poller.create_update_generator.return_value = [
        MagicMock(
            parse_portion=lambda keys, as_json: [
                {"id": "123", "name": "Test Result"},
                {"id": "456", "name": "Another Result"},
            ]
        )
    ]

    args = {"query": "test_query", "collection_name": "test_collection"}

    result = local_search_command(mock_client, args)

    assert isinstance(result, CommandResults)
    assert result.outputs_prefix == "GIBTI.search.local"
    assert result.outputs_key_field == "id"
    outputs = cast(list[dict[str, Any]], result.outputs)
    assert len(outputs) == 2
    assert outputs[0]["id"] == "123"
    assert outputs[0]["additional_info"] == "Name: Test Result"
    assert "Search results" in result.readable_output
    assert "Name: Test Result" in result.readable_output
    assert "Name: Another Result" in result.readable_output


# Unit tests for CommonHelpers


def test_transform_dict_empty():
    assert CommonHelpers.transform_dict({}) == [{}]


def test_transform_dict_various_lengths():
    input_dict: dict[str, Any] = {"a": [1, 2], "b": "x", "c": []}
    result = CommonHelpers.transform_dict(cast(Any, input_dict))
    assert len(result) == 2
    assert result[0] == {"a": 1, "b": "x", "c": None}
    assert result[1] == {"a": 2, "b": "x", "c": None}


def test_remove_underscore_and_lowercase_keys():
    data = [{"Test_Key": 1, "another_key": 2}]
    result = CommonHelpers.remove_underscore_and_lowercase_keys(data)
    assert result == [{"testkey": 1, "anotherkey": 2}]


def test_replace_empty_values_dict():
    data = {"a": "", "b": "value", "c": {"d": ""}}
    result = CommonHelpers.replace_empty_values(data)
    assert result == {"a": None, "b": "value", "c": {"d": None}}


def test_replace_empty_values_list():
    data: list[Any] = ["", "x", [], [{}]]
    result = CommonHelpers.replace_empty_values(cast(Any, data))
    assert result == [None, "x", None, [{}]]


def test_replace_empty_values_empty_list_returns_none():
    assert CommonHelpers.replace_empty_values(cast(Any, [])) is None
    assert CommonHelpers.replace_empty_values(cast(Any, [[]])) is None


def test_all_lists_empty_true():
    data = {"a": [], "b": {"c": []}}
    assert CommonHelpers.all_lists_empty(data) is True


def test_all_lists_empty_false():
    data = {"a": [1], "b": {}}
    assert CommonHelpers.all_lists_empty(data) is False


def test_date_parse_valid():
    result = CommonHelpers.date_parse("2020-01-01", "date")
    assert result.endswith("Z") or result == "2020-01-01"


def test_date_parse_invalid():
    with pytest.raises(Exception):
        CommonHelpers.date_parse("invalid", "date")


def test_transform_list_to_str():
    data: list[dict[str, Any]] = [{"x": [1, 2], "y": "a"}, {"x": []}]
    result = CommonHelpers.transform_list_to_str(cast(Any, data))
    assert result[0]["x"] == "1, 2"
    assert result[1]["x"] == ""


def test_validate_collections_valid():
    CommonHelpers.validate_collections("valid_collection")


def _get_date_field_for_collection(collection_name: str) -> str:
    """
    Helper function to get the appropriate date field for a collection.

    Returns the first date field from INCIDENT_CREATED_DATES_MAPPING for the given collection.
    """
    date_field = INCIDENT_CREATED_DATES_MAPPING.get(collection_name, "dateFirstSeen")
    if isinstance(date_field, list):
        return str(date_field[0])  # Return first field if it's a list
    return str(date_field)


def test_fetch_incidents_with_combolist_and_unique_parameters(mocker, session_fixture):
    """
    Test for verifying fetch_incidents_command correctly passes combolist and unique parameters.

    Given:
      - A session_fixture providing a client and collection name.
      - combolist and unique parameters set to True.

    When:
      - fetch_incidents_command() is called with combolist=True and unique=True.

    Then:
      - Verifies that create_poll_generator is called with the correct combolist and unique parameters.
      - Ensures incidents are returned as a list.
    """
    collection_name, client = session_fixture
    mock_portions = []
    mock_portion = MagicMock()
    mock_portion.sequpdate = 12345
    mock_portion.portion_size = 10
    mock_portion.count = 10
    # Mock incident data that will be processed by IncidentBuilder
    # Include required fields: id, name, evaluation, and date field based on collection
    # Use date format that matches real data: "YYYY-MM-DD" or "YYYY-MM-DDTHH:MM:SS+00:00"
    date_field = _get_date_field_for_collection(collection_name)
    mock_incident_data = {
        "id": "test-id",
        "name": "test",
        "evaluation": {"severity": "green"},
    }
    # Add the appropriate date field for this collection
    mock_incident_data[date_field] = "2023-01-01T00:00:00+00:00"
    # For compromised/breached collection, add emails field required for portal link generation
    if collection_name == "compromised/breached":
        mock_incident_data["emails"] = ["test@example.com"]
    mock_portion.bulk_parse_portion.return_value = [mock_incident_data]
    mock_portions.append(mock_portion)

    mocker.patch.object(client, "get_available_collections_proxy_function", return_value=AVALIBLE_COLLECTIONS_RAW_JSON)
    mocker.patch.object(
        client.poller,
        "get_available_collections",
        return_value=[collection_name],
    )
    mocker.patch.object(
        client,
        "create_poll_generator",
        return_value=(mock_portions, None),
    )

    next_run, incidents = fetch_incidents_command(
        client=client,
        last_run={},
        first_fetch_time="3 days",
        incident_collections=[collection_name],
        max_requests=3,
        hunting_rules=0,
        combolist=True,
        unique=True,
        enable_probable_corporate_access=False,
    )

    # Verify create_poll_generator was called with combolist and unique parameters
    client.create_poll_generator.assert_called_once()
    call_kwargs = client.create_poll_generator.call_args[1]
    assert call_kwargs["combolist"] is True, "Expected combolist parameter to be True."
    assert call_kwargs["unique"] is True, "Expected unique parameter to be True."
    assert isinstance(incidents, list), "Expected incidents to be a list."


def test_fetch_incidents_sequpdate_resolution(mocker, session_fixture):
    """
    Test for verifying sequpdate resolution in create_poll_generator when no last_fetch exists.

    Given:
      - A session_fixture providing a client and collection name.
      - An empty last_run dictionary (first time fetch).
      - A mocked get_seq_update_dict that returns a sequpdate value.

    When:
      - fetch_incidents_command() is called with first_fetch_time.

    Then:
      - Verifies that create_poll_generator resolves sequpdate via get_seq_update_dict.
      - Ensures the resolved sequpdate is used instead of date_from.
    """
    collection_name, client = session_fixture
    mock_portions = []
    mock_portion = MagicMock()
    mock_portion.sequpdate = 12345
    mock_portion.portion_size = 10
    mock_portion.count = 10
    # Mock incident data with required fields
    date_field = _get_date_field_for_collection(collection_name)
    mock_incident_data = {
        "id": "test-id",
        "name": "test",
        "evaluation": {"severity": "green"},
    }
    # Add the appropriate date field for this collection
    mock_incident_data[date_field] = "2023-01-01T00:00:00+00:00"
    if collection_name == "compromised/breached":
        mock_incident_data["emails"] = ["test@example.com"]
    mock_portion.bulk_parse_portion.return_value = [mock_incident_data]
    mock_portions.append(mock_portion)

    # Mock get_seq_update_dict to return a sequpdate
    resolved_sequpdate = 10000
    mocker.patch.object(
        client.poller,
        "get_seq_update_dict",
        return_value={collection_name: resolved_sequpdate},
    )
    mocker.patch.object(client, "get_available_collections_proxy_function", return_value=AVALIBLE_COLLECTIONS_RAW_JSON)
    mocker.patch.object(
        client.poller,
        "get_available_collections",
        return_value=[collection_name],
    )
    if collection_name == "compromised/breached":
        mocker.patch.object(
            client.poller,
            "create_search_generator",
            return_value=mock_portions,
        )
    else:
        mocker.patch.object(
            client.poller,
            "create_update_generator",
            return_value=mock_portions,
        )

    next_run, incidents = fetch_incidents_command(
        client=client,
        last_run={},
        first_fetch_time="2023-01-01",
        incident_collections=[collection_name],
        max_requests=3,
        hunting_rules=0,
        combolist=False,
        unique=False,
        enable_probable_corporate_access=False,
    )

    if collection_name == "compromised/breached":
        client.poller.get_seq_update_dict.assert_not_called()
        client.poller.create_search_generator.assert_called_once()
        search_call_kwargs = client.poller.create_search_generator.call_args[1]
        assert search_call_kwargs["date_from"] == "2023-01-01"
        assert search_call_kwargs["apply_hunting_rules"] == 1
        assert isinstance(next_run["last_fetch"][collection_name], dict)
    else:
        # Verify get_seq_update_dict was called for sequpdate resolution
        client.poller.get_seq_update_dict.assert_called_once()
    assert isinstance(incidents, list), "Expected incidents to be a list."


def test_fetch_incidents_effective_last_fetch_calculation(mocker, session_fixture):
    """
    Test for verifying effective_last_fetch calculation using max(last_fetch, sequpdate).

    Given:
      - A session_fixture providing a client and collection name.
      - A last_run dictionary with existing last_fetch value.
      - Multiple portions with different sequpdate values.

    When:
      - fetch_incidents_command() processes portions and updates sequpdate.

    Then:
      - Verifies that next_run contains the maximum of last_fetch and sequpdate.
      - Ensures effective_last_fetch is correctly calculated.
    """
    collection_name, client = session_fixture
    mock_portions = []
    mock_portion = MagicMock()
    if collection_name == "compromised/breached":
        last_fetch_value = {
            "starting_date_from": "2023-01-01",
            "starting_date_to": "2023-01-31",
            "current_date_to": "2023-01-31",
        }
        sequpdate_value = None
    else:
        last_fetch_value = 10000
        sequpdate_value = 15000  # Higher than last_fetch
    mock_portion.sequpdate = sequpdate_value
    mock_portion.portion_size = 10
    mock_portion.count = 10
    # Mock incident data with required fields
    date_field = _get_date_field_for_collection(collection_name)
    mock_incident_data = {
        "id": "test-id",
        "name": "test",
        "evaluation": {"severity": "green"},
    }
    # Add the appropriate date field for this collection
    mock_incident_data[date_field] = "2023-01-01T00:00:00+00:00"
    if collection_name == "compromised/breached":
        mock_incident_data["emails"] = ["test@example.com"]
    mock_portion.bulk_parse_portion.return_value = [mock_incident_data]
    mock_portions.append(mock_portion)

    mocker.patch.object(client, "get_available_collections_proxy_function", return_value=AVALIBLE_COLLECTIONS_RAW_JSON)
    mocker.patch.object(
        client.poller,
        "get_available_collections",
        return_value=[collection_name],
    )
    if collection_name == "compromised/breached":
        mocker.patch.object(
            client.poller,
            "create_search_generator",
            return_value=mock_portions,
        )
    else:
        mocker.patch.object(
            client,
            "create_poll_generator",
            return_value=(mock_portions, last_fetch_value),
        )

    next_run, incidents = fetch_incidents_command(
        client=client,
        last_run={"last_fetch": {collection_name: last_fetch_value}},
        first_fetch_time="3 days",
        incident_collections=[collection_name],
        max_requests=3,
        hunting_rules=0,
        combolist=False,
        unique=False,
        enable_probable_corporate_access=False,
    )

    assert collection_name in next_run["last_fetch"], "Expected collection name in next_run['last_fetch']."
    effective_last_fetch = next_run["last_fetch"][collection_name]
    if collection_name == "compromised/breached":
        client.poller.create_search_generator.assert_called_once()
        search_call_kwargs = client.poller.create_search_generator.call_args[1]
        assert search_call_kwargs["date_from"] is None
        assert search_call_kwargs["date_to"] == last_fetch_value["current_date_to"]
        assert effective_last_fetch == last_fetch_value
    else:
        assert int(str(effective_last_fetch)) == max(
            last_fetch_value, sequpdate_value
        ), f"Expected effective_last_fetch to be max({last_fetch_value}, {sequpdate_value}) = {sequpdate_value}."


def test_fetch_incidents_incident_processing_loop(mocker, session_fixture):
    """
    Test for verifying the incident processing loop handles multiple portions correctly.

    Given:
      - A session_fixture providing a client and collection name.
      - Multiple portions with different sequpdate values.

    When:
      - fetch_incidents_command() processes multiple portions in a loop.

    Then:
      - Verifies that all portions are processed.
      - Ensures sequpdate is updated from each portion.
      - Checks that requests_count limits the number of processed portions.
    """
    collection_name, client = session_fixture
    # Create multiple mock portions
    mock_portions = []
    date_field = _get_date_field_for_collection(collection_name)
    for i in range(5):
        mock_portion = MagicMock()
        mock_portion.sequpdate = 10000 + i * 1000
        mock_portion.portion_size = 10
        mock_portion.count = 10
        # Mock incident data with required fields
        mock_incident_data = {
            "id": f"test-id-{i}",
            "name": f"test-{i}",
            "evaluation": {"severity": "green"},
        }
        # Add the appropriate date field for this collection
        mock_incident_data[date_field] = "2023-01-01T00:00:00+00:00"
        mock_portion.bulk_parse_portion.return_value = [mock_incident_data]
        mock_portions.append(mock_portion)

    mocker.patch.object(client, "get_available_collections_proxy_function", return_value=AVALIBLE_COLLECTIONS_RAW_JSON)
    mocker.patch.object(
        client.poller,
        "get_available_collections",
        return_value=[collection_name],
    )
    mocker.patch.object(
        client,
        "create_poll_generator",
        return_value=(mock_portions, None),
    )

    max_requests = 3
    next_run, incidents = fetch_incidents_command(
        client=client,
        last_run={},
        first_fetch_time="3 days",
        incident_collections=[collection_name],
        max_requests=max_requests,
        hunting_rules=0,
        combolist=False,
        unique=False,
        enable_probable_corporate_access=False,
    )

    # Verify that only max_requests portions were processed
    assert len(incidents) == max_requests, f"Expected {max_requests} incidents, got {len(incidents)}."
    # Verify that the final sequpdate is from the last processed portion
    assert collection_name in next_run["last_fetch"], "Expected collection name in next_run['last_fetch']."


def test_create_poll_generator_with_combolist_and_unique(mocker, single_session_fixture):
    """
    Test for verifying create_poll_generator correctly passes combolist and unique to create_update_generator.

    Given:
      - A client instance.
      - combolist=True and unique=True parameters.

    When:
      - create_poll_generator() is called with these parameters.

    Then:
      - Verifies that create_update_generator is called with combolist=1 and unique=1 (converted to int).
    """
    client = single_session_fixture
    collection_name = "compromised/account_group"

    mock_portions = []
    mock_portion = MagicMock()
    mock_portions.append(mock_portion)

    mocker.patch.object(
        client.poller,
        "get_seq_update_dict",
        return_value={},  # Empty dict means no sequpdate found, will use date_from
    )
    mocker.patch.object(
        client.poller,
        "create_update_generator",
        return_value=mock_portions,
    )

    portions, last_fetch = client.create_poll_generator(
        collection_name=collection_name,
        hunting_rules=0,
        enable_probable_corporate_access=False,
        unique=True,
        combolist=True,
        last_fetch=None,
        first_fetch_time="2023-01-01",
    )

    # Verify create_update_generator was called with combolist and unique as integers
    client.poller.create_update_generator.assert_called_once()
    call_kwargs = client.poller.create_update_generator.call_args[1]
    assert call_kwargs["combolist"] == 1, "Expected combolist to be converted to 1 (int)."
    assert call_kwargs["unique"] == 1, "Expected unique to be converted to 1 (int)."
    assert portions == mock_portions, "Expected returned portions to match mocked portions."


def test_create_poll_generator_sequpdate_resolution_success(mocker, single_session_fixture):
    """
    Test for verifying create_poll_generator resolves sequpdate via get_seq_update_dict when successful.

    Given:
      - A client instance.
      - No last_fetch, but date_from is provided.
      - get_seq_update_dict returns a valid sequpdate.

    When:
      - create_poll_generator() is called with first_fetch_time.

    Then:
      - Verifies that get_seq_update_dict is called.
      - Ensures resolved sequpdate is used and date_from is set to None.
    """
    client = single_session_fixture
    collection_name = "compromised/account_group"
    resolved_sequpdate = 12345

    mock_portions = []
    mock_portion = MagicMock()
    mock_portions.append(mock_portion)

    mocker.patch.object(
        client.poller,
        "get_seq_update_dict",
        return_value={collection_name: resolved_sequpdate},
    )
    mocker.patch.object(
        client.poller,
        "create_update_generator",
        return_value=mock_portions,
    )

    portions, last_fetch = client.create_poll_generator(
        collection_name=collection_name,
        hunting_rules=0,
        enable_probable_corporate_access=False,
        unique=False,
        combolist=False,
        last_fetch=None,
        first_fetch_time="2023-01-01",
    )

    # Verify get_seq_update_dict was called
    client.poller.get_seq_update_dict.assert_called_once()
    # Verify create_update_generator was called with resolved sequpdate and date_from=None
    call_kwargs = client.poller.create_update_generator.call_args[1]
    assert call_kwargs["sequpdate"] == resolved_sequpdate, "Expected resolved sequpdate to be used."
    assert call_kwargs.get("date_from") is None, "Expected date_from to be None when sequpdate is resolved."


def test_create_poll_generator_sequpdate_resolution_fallback(mocker, single_session_fixture):
    """
    Test for verifying create_poll_generator falls back to date_from when sequpdate resolution fails.

    Given:
      - A client instance.
      - No last_fetch, but date_from is provided.
      - get_seq_update_dict returns empty dict or raises exception.

    When:
      - create_poll_generator() is called with first_fetch_time.

    Then:
      - Verifies that get_seq_update_dict is called.
      - Ensures date_from is used when sequpdate resolution fails.
    """
    client = single_session_fixture
    collection_name = "compromised/account_group"

    mock_portions = []
    mock_portion = MagicMock()
    mock_portions.append(mock_portion)

    mocker.patch.object(
        client.poller,
        "get_seq_update_dict",
        return_value={},  # Empty dict means no sequpdate found
    )
    mocker.patch.object(
        client.poller,
        "create_update_generator",
        return_value=mock_portions,
    )

    portions, last_fetch = client.create_poll_generator(
        collection_name=collection_name,
        hunting_rules=0,
        enable_probable_corporate_access=False,
        unique=False,
        combolist=False,
        last_fetch=None,
        first_fetch_time="2023-01-01",
    )

    # Verify get_seq_update_dict was called
    client.poller.get_seq_update_dict.assert_called_once()
    # Verify create_update_generator was called with date_from (fallback)
    call_kwargs = client.poller.create_update_generator.call_args[1]
    assert call_kwargs.get("date_from") is not None, "Expected date_from to be used when sequpdate resolution fails."
    assert call_kwargs.get("sequpdate") is None, "Expected sequpdate to be None when resolution fails."


def test_create_poll_generator_compromised_breached_uses_search_generator(mocker, single_session_fixture):
    client = single_session_fixture
    collection_name = "compromised/breached"
    mock_portions = [MagicMock()]

    mocker.patch.object(
        client.poller,
        "create_search_generator",
        return_value=mock_portions,
    )

    portions, last_fetch = client.create_poll_generator(
        collection_name=collection_name,
        hunting_rules=0,
        enable_probable_corporate_access=False,
        unique=False,
        combolist=False,
        last_fetch=None,
        first_fetch_time="2023-01-01",
    )

    client.poller.create_search_generator.assert_called_once()
    call_kwargs = client.poller.create_search_generator.call_args[1]
    assert call_kwargs["date_from"] == "2023-01-01"
    assert call_kwargs["apply_hunting_rules"] == 1
    assert portions == mock_portions
    assert last_fetch["starting_date_from"] == "2023-01-01"
    assert last_fetch["starting_date_to"] == last_fetch["current_date_to"]


def test_fetch_incidents_compromised_breached_keeps_date_range_last_fetch(mocker, single_session_fixture):
    collection_name = "compromised/breached"
    client = single_session_fixture
    mock_portion = MagicMock()
    mock_portion.sequpdate = None
    mock_portion.portion_size = 1
    mock_portion.count = 1
    mock_portion.bulk_parse_portion.return_value = [
        {
            "id": "breached-id",
            "name": ["Email collection"],
            "emails": ["user@example.com"],
            "uploadTime": "2024-10-01T01:45:13",
            "evaluation": {"severity": "green"},
        }
    ]
    expected_last_fetch = {
        "starting_date_from": "2024-10-01",
        "starting_date_to": "2024-10-31",
        "current_date_to": "2024-10-31",
    }

    mocker.patch.object(client.poller, "get_available_collections", return_value=[collection_name])
    mocker.patch.object(client, "create_poll_generator", return_value=([mock_portion], expected_last_fetch))

    next_run, incidents = fetch_incidents_command(
        client=client,
        last_run={},
        first_fetch_time="3 days",
        incident_collections=[collection_name],
        max_requests=3,
        hunting_rules=0,
    )

    assert len(incidents) == 1
    assert next_run["last_fetch"][collection_name] == expected_last_fetch


def test_build_feed_compromised_breached_generates_portal_link_from_email(mocker, single_session_fixture):
    client = single_session_fixture
    collection_name = "compromised/breached"
    mock_result = MagicMock()
    mock_result.parse_portion.return_value = {
        "id": "breached-id",
        "name": ["Email collection"],
        "emails": ["user@example.com"],
        "uploadTime": "2024-10-01T01:45:13",
        "evaluation": {"severity": "green"},
        "portalLink": "https://tap.group-ib.com/cd/breached?id=breached-id",
    }

    mocker.patch.object(client.poller, "search_feed_by_id", return_value=mock_result)

    feed, _, _, _, _ = BuilderCommandResponses(
        client=client,
        collection_name=collection_name,
        args={"id": "breached-id"},
    ).build_feed()

    assert feed["portalLink"] == f"{PORTAL_LINKS[collection_name]}user@example.com"


def test_build_incident_compromised_breached_generates_portal_link_from_email():
    collection_name = "compromised/breached"
    incident = {
        "id": "breached-id",
        "name": ["Email collection"],
        "emails": ["user@example.com"],
        "uploadTime": "2024-10-01T01:45:13",
        "evaluation": {"severity": "green"},
        "portalLink": "https://tap.group-ib.com/cd/breached?id=breached-id",
    }

    built_incident = IncidentBuilder(
        collection_name=collection_name,
        incident=incident,
        mapping=MAPPING[collection_name],
    ).build_incident()
    raw_incident = loads(built_incident["rawJSON"])

    assert raw_incident["portalLink"] == f"{PORTAL_LINKS[collection_name]}user@example.com"


# ---------------------------------------------------------------------------
# Deduplication: retention contract for `dedup_lookback_days`
# ---------------------------------------------------------------------------
#
# These tests pin the 1:1 semantics of the user-facing parameter:
#   * Configured `dedup_lookback_days = N` means "an ID added today is dropped
#     exactly N days later".
#   * No hidden multipliers, no "latest ID kept forever" exception.
#
# The previous implementation relied on `CommonServerPython.get_found_incident_ids`,
# which silently doubled the retention window (`look_back * 2`) and pinned the
# newest ID forever, breaking the contract documented in the integration YAML.


SECONDS_PER_DAY = 86_400


def test_convert_dedup_lookback_days_to_seconds_basic():
    assert GroupIBTIA._convert_dedup_lookback_days_to_seconds(1) == SECONDS_PER_DAY
    assert GroupIBTIA._convert_dedup_lookback_days_to_seconds(365) == 365 * SECONDS_PER_DAY


def test_convert_dedup_lookback_days_to_seconds_zero():
    assert GroupIBTIA._convert_dedup_lookback_days_to_seconds(0) == 0


def test_prune_seen_ids_returns_empty_for_zero_retention():
    cache = {"a": 1000.0, "b": 2000.0}
    pruned = GroupIBTIA._prune_seen_incident_ids(cache, retention_seconds=0, now=10_000.0)
    assert pruned == {}


def test_prune_seen_ids_returns_empty_for_negative_retention():
    cache = {"a": 1000.0}
    pruned = GroupIBTIA._prune_seen_incident_ids(cache, retention_seconds=-1, now=10_000.0)
    assert pruned == {}


def test_prune_seen_ids_keeps_entries_within_window():
    now = 10_000.0
    cache = {
        "fresh": now - 100,
        "older": now - 500,
    }
    pruned = GroupIBTIA._prune_seen_incident_ids(cache, retention_seconds=1000, now=now)
    assert pruned == {"fresh": now - 100, "older": now - 500}


def test_prune_seen_ids_drops_entries_older_than_window():
    now = 10_000.0
    cache = {
        "fresh": now - 100,
        "stale": now - 5000,
    }
    pruned = GroupIBTIA._prune_seen_incident_ids(cache, retention_seconds=1000, now=now)
    assert pruned == {"fresh": now - 100}


def test_prune_seen_ids_threshold_is_inclusive():
    """An ID exactly at the retention boundary must be kept (>=, not >)."""
    now = 10_000.0
    cache = {"boundary": now - 1000}
    pruned = GroupIBTIA._prune_seen_incident_ids(cache, retention_seconds=1000, now=now)
    assert pruned == {"boundary": now - 1000}


def test_prune_seen_ids_drops_entries_just_past_threshold():
    now = 10_000.0
    cache = {"just_past": now - 1000.001}
    pruned = GroupIBTIA._prune_seen_incident_ids(cache, retention_seconds=1000, now=now)
    assert pruned == {}


def test_prune_seen_ids_drops_entries_with_malformed_timestamp():
    """Defensive: corrupt cache entries are dropped, never raised."""
    now = 10_000.0
    cache = {
        "ok": now - 100,
        "string_ts": "not-a-number",  # type: ignore[dict-item]
        "none_ts": None,  # type: ignore[dict-item]
        "negative": -1,
    }
    pruned = GroupIBTIA._prune_seen_incident_ids(cache, retention_seconds=1000, now=now)  # type: ignore[arg-type]
    assert pruned == {"ok": now - 100}


def test_prune_seen_ids_does_not_mutate_input():
    cache = {"a": 1.0, "b": 2.0}
    snapshot = dict(cache)
    GroupIBTIA._prune_seen_incident_ids(cache, retention_seconds=1, now=1000.0)
    assert cache == snapshot


def test_prune_seen_ids_drops_latest_id_when_older_than_retention():
    """
    Critical regression vs the old CommonServerPython helper, which pinned the
    newest ID forever via `addition_time == latest_incident_time`.

    With the in-house helper, every entry obeys the retention window without
    exceptions; otherwise, a single never-re-fetched ID would grow the cache
    unboundedly across years of operation.
    """
    now = 10_000.0
    cache = {
        "ancient_but_latest": now - 99_999_999,
        "ancient_too": now - 99_999_998,
    }
    pruned = GroupIBTIA._prune_seen_incident_ids(cache, retention_seconds=1000, now=now)
    assert pruned == {}


def test_update_fetch_seen_ids_cache_noop_for_empty_incidents():
    state = {"found_incident_ids": {"existing": 1.0}}
    GroupIBTIA._update_fetch_seen_incident_ids_cache(
        last_run_state=state,
        incidents=[],
        dedup_lookback_days=365,
    )
    assert state == {"found_incident_ids": {"existing": 1.0}}


def test_update_fetch_seen_ids_cache_adds_new_ids(mocker):
    fixed_now = 1_700_000_000.0
    mocker.patch.object(GroupIBTIA.time, "time", return_value=fixed_now)

    state: dict = {}
    GroupIBTIA._update_fetch_seen_incident_ids_cache(
        last_run_state=state,
        incidents=[{"id": "alpha"}, {"id": "beta"}],
        dedup_lookback_days=365,
    )
    assert state["found_incident_ids"] == {"alpha": fixed_now, "beta": fixed_now}


def test_update_fetch_seen_ids_cache_normalizes_non_string_ids(mocker):
    fixed_now = 1_700_000_000.0
    mocker.patch.object(GroupIBTIA.time, "time", return_value=fixed_now)

    state: dict = {}
    GroupIBTIA._update_fetch_seen_incident_ids_cache(
        last_run_state=state,
        incidents=[{"id": 42}, {"id": "alpha"}],
        dedup_lookback_days=365,
    )
    assert state["found_incident_ids"] == {"42": fixed_now, "alpha": fixed_now}


def test_update_fetch_seen_ids_cache_skips_incidents_without_id(mocker):
    fixed_now = 1_700_000_000.0
    mocker.patch.object(GroupIBTIA.time, "time", return_value=fixed_now)

    state: dict = {}
    GroupIBTIA._update_fetch_seen_incident_ids_cache(
        last_run_state=state,
        incidents=[{"id": None}, {"name": "no-id"}, {"id": "ok"}],
        dedup_lookback_days=365,
    )
    assert state["found_incident_ids"] == {"ok": fixed_now}


def test_update_fetch_seen_ids_cache_prunes_old_entries(mocker):
    fixed_now = 1_700_000_000.0
    mocker.patch.object(GroupIBTIA.time, "time", return_value=fixed_now)

    state = {
        "found_incident_ids": {
            "stale": fixed_now - (366 * SECONDS_PER_DAY),  # > 365d -> drop
            "fresh": fixed_now - (10 * SECONDS_PER_DAY),  # well within window
        }
    }
    GroupIBTIA._update_fetch_seen_incident_ids_cache(
        last_run_state=state,
        incidents=[{"id": "new"}],
        dedup_lookback_days=365,
    )
    assert "stale" not in state["found_incident_ids"]
    assert "fresh" in state["found_incident_ids"]
    assert state["found_incident_ids"]["new"] == fixed_now


def test_update_fetch_seen_ids_cache_handles_corrupt_existing_value(mocker):
    """If `found_incident_ids` was somehow stored as a string, we don't crash."""
    fixed_now = 1_700_000_000.0
    mocker.patch.object(GroupIBTIA.time, "time", return_value=fixed_now)

    state: dict = {"found_incident_ids": "this-should-have-been-a-dict"}
    GroupIBTIA._update_fetch_seen_incident_ids_cache(
        last_run_state=state,
        incidents=[{"id": "alpha"}],
        dedup_lookback_days=365,
    )
    assert state["found_incident_ids"] == {"alpha": fixed_now}


def test_update_fetch_seen_ids_cache_one_to_one_retention_contract(mocker):
    """
    THE contract test for `dedup_lookback_days`:

    Configured retention is N days. An ID added exactly N days ago must
    still be kept. An ID added N days + 1 second ago must be dropped.

    This is the assertion the previous implementation could not satisfy:
    CommonServerPython would have kept the older ID for 2N days, and pinned
    the newest one forever.
    """
    fixed_now = 1_700_000_000.0
    mocker.patch.object(GroupIBTIA.time, "time", return_value=fixed_now)

    n_days = 7
    retention_seconds = n_days * SECONDS_PER_DAY

    state = {
        "found_incident_ids": {
            "boundary": fixed_now - retention_seconds,
            "just_past": fixed_now - retention_seconds - 1,
        }
    }
    GroupIBTIA._update_fetch_seen_incident_ids_cache(
        last_run_state=state,
        incidents=[{"id": "today"}],
        dedup_lookback_days=n_days,
    )

    cache = state["found_incident_ids"]
    assert "boundary" in cache, "ID at exactly N days must be retained (>= threshold)"
    assert "just_past" not in cache, "ID at N days + 1s must be pruned"
    assert cache["today"] == fixed_now


def test_update_fetch_seen_ids_cache_disables_when_retention_is_zero(mocker):
    """
    Operational kill-switch: setting `dedup_lookback_days = 0` must drop the
    entire cache on the next update so customers can fully disable built-in
    deduplication without manual cleanup.
    """
    fixed_now = 1_700_000_000.0
    mocker.patch.object(GroupIBTIA.time, "time", return_value=fixed_now)

    state = {"found_incident_ids": {"old": fixed_now - 10}}
    GroupIBTIA._update_fetch_seen_incident_ids_cache(
        last_run_state=state,
        incidents=[{"id": "incoming"}],
        dedup_lookback_days=0,
    )
    assert state["found_incident_ids"] == {}


# ---------------------------------------------------------------------------
# Reputation pipeline — defensive behaviour on null / empty API responses
# ---------------------------------------------------------------------------
#
# Group-IB API legitimately returns HTTP 200 with a `null` (or `[]`) body
# when no data is available for an indicator. The reputation pipeline must
# never raise; it must return a clean DBotScore.NONE with a "no data"
# readable_output. The tests below pin that contract.


def _make_reputation_processor(client):
    """Construct a ReputationCommandProcessor with a stub args context."""
    return GroupIBTIA.ReputationCommandProcessor(
        client=client,
        args={"value": "example.com"},
    )


@pytest.mark.parametrize(
    "search_return",
    [None, "", [], {}, {"oops": 1}, [None, "trash", {"apiPath": "x", "count": 1}]],
)
def test_get_search_data_tolerates_null_or_garbage(single_session_fixture, mocker, search_return):
    """Every null / empty / non-list / mixed response collapses to a clean list."""
    client = single_session_fixture
    mocker.patch.object(client.poller, "global_search", return_value=search_return)
    processor = _make_reputation_processor(client)

    result = processor._get_search_data("example.com")
    # The only entry surviving from the mixed payload is {"apiPath":"x","count":1}.
    expected = [("x", 1)] if search_return == [None, "trash", {"apiPath": "x", "count": 1}] else []
    assert result == expected


def test_get_search_data_swallows_global_search_exception(single_session_fixture, mocker):
    client = single_session_fixture
    mocker.patch.object(client.poller, "global_search", side_effect=RuntimeError("boom"))
    processor = _make_reputation_processor(client)
    assert processor._get_search_data("example.com") == []


def test_get_indicator_data_handles_empty_search_data(single_session_fixture, mocker):
    """An empty search_data must not crash; returns just {} (no per-collection work)."""
    client = single_session_fixture
    processor = _make_reputation_processor(client)
    out = processor._get_indicator_data("domain", "example.com", [])
    assert out == {}


def test_get_indicator_data_skips_collection_on_exception(single_session_fixture, mocker):
    """One failing collection must not abort the rest of the lookup."""
    client = single_session_fixture
    mocker.patch.object(
        GroupIBTIA.IndicatorsHelper,
        "collect_portions_for_indicator",
        side_effect=RuntimeError("upstream null"),
    )
    processor = _make_reputation_processor(client)
    # Pass at least one allowed path; the exception is swallowed, returns [].
    out = processor._get_indicator_data("domain", "example.com", [("ioc/common", 5)])
    assert out == {"ioc/common": []}


def test_get_indicator_data_handles_ip_enrichment_failure(single_session_fixture, mocker):
    client = single_session_fixture
    mocker.patch.object(GroupIBTIA.IndicatorsHelper, "build_ip_enrichment", side_effect=RuntimeError("ip enrich boom"))
    processor = _make_reputation_processor(client)
    # No collections returned; ip-enrichment branch is exercised and the
    # exception swallowed -> data_per_collections stays empty.
    out = processor._get_indicator_data("ip", "8.8.8.8", [])
    assert out == {}


def test_collect_portions_handles_none_portions(single_session_fixture, mocker):
    client = single_session_fixture
    mocker.patch.object(client.poller, "create_update_generator", return_value=None)
    out = GroupIBTIA.IndicatorsHelper.collect_portions_for_indicator(
        indicator_name="domain",
        indicator_value="example.com",
        path="ioc/common",
        poller=client.poller,
        dates_mapping=None,
        sensitive_collections=None,
    )
    assert out == []


def test_collect_portions_handles_none_inner_portion(single_session_fixture, mocker):
    client = single_session_fixture

    class _P:
        raw_dict = None

    mocker.patch.object(client.poller, "create_update_generator", return_value=[None, _P()])
    out = GroupIBTIA.IndicatorsHelper.collect_portions_for_indicator(
        indicator_name="domain",
        indicator_value="example.com",
        path="ioc/common",
        poller=client.poller,
        dates_mapping=None,
        sensitive_collections=None,
    )
    # Both portions yield no usable data; collector returns an empty list,
    # never raises.
    assert out == []


def test_build_ip_enrichment_handles_null_scoring(single_session_fixture, mocker):
    client = single_session_fixture
    mocker.patch.object(client.poller, "scoring", return_value=None)
    mocker.patch.object(client.poller, "graph_ip_search", return_value=None)
    data = GroupIBTIA.IndicatorsHelper.build_ip_enrichment(poller=client.poller, indicator_value="8.8.8.8", mapping={})
    # scoring null -> data["scoring"]["score"] == None, no crash.
    assert data == {"scoring": {"score": None}}


def test_build_ip_enrichment_handles_scoring_exception(single_session_fixture, mocker):
    client = single_session_fixture
    mocker.patch.object(client.poller, "scoring", side_effect=RuntimeError("scoring boom"))
    mocker.patch.object(client.poller, "graph_ip_search", return_value=None)
    data = GroupIBTIA.IndicatorsHelper.build_ip_enrichment(poller=client.poller, indicator_value="8.8.8.8", mapping={})
    assert data == {"scoring": {"score": None}}


def test_global_search_command_tolerates_null_response(single_session_fixture, mocker):
    """A null response from global_search must yield a clean 'No results' CommandResults."""
    client = single_session_fixture
    mocker.patch.object(client, "search_proxy_function", return_value=None)
    result = GroupIBTIA.global_search_command(client=client, args={"query": "example.com"})
    assert isinstance(result, CommandResults)
    assert result.outputs == []
    assert "No results" in (result.readable_output or "")


def test_global_search_command_tolerates_empty_list(single_session_fixture, mocker):
    client = single_session_fixture
    mocker.patch.object(client, "search_proxy_function", return_value=[])
    result = GroupIBTIA.global_search_command(client=client, args={"query": "example.com"})
    assert result.outputs == []


def test_global_search_command_skips_non_dict_entries(single_session_fixture, mocker):
    client = single_session_fixture
    # MAPPING is checked for the apiPath; we pass a known one + garbage.
    known_path = next(iter(GroupIBTIA.MAPPING.keys()))
    raw = [None, "trash", 42, {"apiPath": known_path, "count": 3, "link": "http://x"}]
    mocker.patch.object(client, "search_proxy_function", return_value=raw)
    result = GroupIBTIA.global_search_command(client=client, args={"query": "example.com"})
    # Only the well-formed entry survives.
    assert isinstance(result.outputs, list)
    assert len(result.outputs) == 1


def test_local_search_command_tolerates_none_portions(mock_client, mock_common_helpers):
    """`create_update_generator` returning None must yield an empty result list, not crash."""
    mock_client.poller.create_update_generator.return_value = None
    result = GroupIBTIA.local_search_command(mock_client, {"query": "q", "collection_name": "test_collection"})
    assert isinstance(result, CommandResults)
    assert result.outputs == []


def test_local_search_command_skips_none_portion_in_iter(mock_client, mock_common_helpers):
    """Yielding None inside the portion generator must be skipped silently."""
    portion = MagicMock()
    portion.sequpdate = 1
    portion.parse_portion.return_value = None
    mock_client.poller.create_update_generator.return_value = [None, portion]
    result = GroupIBTIA.local_search_command(mock_client, {"query": "q", "collection_name": "test_collection"})
    assert result.outputs == []


def test_reputation_run_no_data_emits_unknown_score(single_session_fixture, mocker):
    """End-to-end: global_search returns null -> DBotScore.NONE (0) + 'No data' readable."""
    client = single_session_fixture
    mocker.patch.object(client.poller, "global_search", return_value=None)
    processor = GroupIBTIA.ReputationCommandProcessor(
        client=client,
        args={"value": "example.com"},
    )

    result = processor.run("domain", GroupIBTIA.DBotScoreType.DOMAIN)
    assert isinstance(result, CommandResults)
    # readable_output carries the "no data" note.
    assert "No Group-IB" in (result.readable_output or "")
    # DBotScore.NONE is the integer 0 in the public XSOAR API.
    assert result.raw_response["score"] == 0