Group-IB Digital Risk Protection

Pack helps to integrate Group-IB Digital Risk Protection and get violations incidents directly into Cortex XSOAR.

Data Enrichment & Threat Intelligence · Group-IB Digital Risk Protection

Details

IDGroup-IB Digital Risk Protection
CategoryData Enrichment & Threat Intelligence
From Version6.10.0
Docker Imagedemisto/vendors-sdk:1.0.0.10120494

README

Pack helps to integrate Group-IB Digital Risk Protection and get violations incidents directly into Cortex XSOAR.
This integration was integrated and tested with version 1.0 of Group-IB Digital Risk Protection.

Configure Group-IB Digital Risk Protection in Cortex

Parameter Description Required
GIB DRP URL   True
Fetch incidents   False
Incident type   False
Trust any certificate (not secure)   False
Use system proxy settings   False
Incidents Fetch Interval   False
Username   True
Password   True
Violation Section to filter the received Violation   False
Brands to filter the received Violation Brands for filtering received violations. The list of available brands can be obtained with the command !gibdrp-get-brands in the menu WarRoom -> Playground. After getting the brands you must specify the brand ID for which you want to receive violations. Attention! Currently filtering is available only by one brand in one Instance False
Incidents first fetch Date to start fetching incidents from. False
Download images Enables or disables loading of each image in each violation. Can significantly affect the speed of data collection if the parameter is enabled, i.e. set to True False
Getting Typosquatting only Allows for the collection of offenses suitable only for Typo Squatting False
Number of requests per collection A number of requests per collection that integration sends in one fetch iteration (each request picks up to 30 incidents). If you face some runtime errors, lower the value. True

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

gibdrp-get-brands


Receive all configured brands.

Base Command

gibdrp-get-brands

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
GIBDRP.OtherInfo string List of configured brands.
GIBDRP.OtherInfo.brands.name string Brand name.
GIBDRP.OtherInfo.brands.id string Brand ID.

Command example


#### Context Example

```json
{
    "GIBDRP": {
        "OtherInfo": {
            "brands": [
                {
                    "id": "PvY1BZUBSFbLZGo2x8TA",
                    "name": "Example Brand"
                }
            ]
        }
    }
}

Human Readable Output

Installed Brands

Name Id
Example Brand PvY1BZUBSFbLZGo2x8TA

gibdrp-get-subscriptions


Receive all configured subscriptions.

Base Command

gibdrp-get-subscriptions

Input

| Argument Name | Description | Required |
| — | — | — |

Context Output

Path Type Description
GIBDRP.OtherInfo string List of configured subscriptions.
GIBDRP.OtherInfo.subscriptions string Subscriptions list.

Command example


#### Context Example

```json
{
    "GIBDRP": {
        "OtherInfo": {
            "subscriptions": [
                "scam"
            ]
        }
    }
}

Human Readable Output

Purchased subscriptions

Subscriptions
scam

gibdrp-get-violation-by-id


Getting a single violation by its ID.

Base Command

gibdrp-get-violation-by-id

Input

Argument Name Description Required
id ID violation. Required

Context Output

Path Type Description
GIBDRP.Violation.id string Violation ID.
GIBDRP.Violation.title string Violation title.
GIBDRP.Violation.description string Violation description.
GIBDRP.Violation.status string Violation status.
GIBDRP.Violation.violation_uri string Violation URI.
GIBDRP.Violation.source string Violation source section.
GIBDRP.Violation.detected date Detected timestamp.

gibdrp-change-violation-status


Changing the status of a single violation.

Base Command

gibdrp-change-violation-status

Input

Argument Name Description Required
id ID violation. Required
status What status to change to. Possible values are: approve, reject. Required

Context Output

There is no context output for this command.

Configuration parameters

  • url — GIB DRP URL (required)
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • incidentFetchInterval — Incidents Fetch Interval
  • credentials — Username (required)
  • violationSection — Filter by Violation Type
  • brands — Filter by Brand
  • first_fetch — Incidents first fetch
  • download_images — Download images
  • only_typosquatting — Get Typosquatting Only
  • max_fetch — Number of requests per collection (required)

Commands (4)

  • gibdrp-change-violation-status

    Changing the status of a single violation.

  • gibdrp-get-brands

    Receive all configured brands.

  • gibdrp-get-subscriptions

    Receive all configured subscriptions.

  • gibdrp-get-violation-by-id

    Getting a single violation by its ID.

import pytest
from urllib3.exceptions import InsecureRequestWarning
from urllib3 import disable_warnings as urllib3_disable_warnings
import GroupIBDigitalRiskProtection
from enum import Enum
import os
from json import load
from cyberintegrations.cyberintegrations import Parser

# Disable insecure warnings
urllib3_disable_warnings(InsecureRequestWarning)

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

BRANDS_RAW_JSON = [
    {"name": "Example Brand 1", "id": "exampleid1223"},
    {"name": "Example Brand 2", "id": "exampleid321"},
]

SUBSCRIPTIONS_RAW_JSON = ["scam", "example"]

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

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

TEST_GET_FILES_BYTES = open(
    f"{realpath}/test_data/get_file_example_5a7bf6ece60ff635c6b844418a0528d97fa7016b362387125a96f4c0bf60a774.jpeg", "rb"
).read()

TEST_VIOLATION_ID = "exampleid"
TEST_FILE_SHA = "5a7bf6ece60ff635c6b844418a0528d97fa7016b362387125a96f4c0bf60a774"


class Commands(Enum):
    GET_BRANDS = "gibdrp-get-brands"
    GET_SUBSCRIPTIONS = "gibdrp-get-subscriptions"
    GET_VIOLATION_BY_ID = "gibdrp-get-violation-by-id"
    CHANGE_VIOLATION_STATUS = "gibdrp-change-violation-status"
    TEST_MODULE = "test-module"
    FETCH_INCIDENTS = "fetch-incidents"


@pytest.fixture(scope="function")
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 GroupIBDigitalRiskProtection.Client(
        base_url="https://drp.group-ib.com/client_api/",
        auth=("example@roup-ib.com", "exampleAPI_TOKEN"),
    )


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):
        GroupIBDigitalRiskProtection.main()["error_command"]()  # type: ignore


""" Clien Testing """


def test_get_brands(mocker, session_fixture):
    client = session_fixture
    mocker.patch.object(client, "get_formatted_brands", return_value=[BRANDS_RAW_JSON])
    response = client.get_formatted_brands()
    assert isinstance(response, list)
    assert len(response) > 0


def test_get_subscriptions(mocker, session_fixture):
    client = session_fixture
    mocker.patch.object(client, "get_formatted_subscriptions", return_value=[SUBSCRIPTIONS_RAW_JSON])
    response = client.get_formatted_subscriptions()
    assert isinstance(response, list)
    assert len(response) > 0


def test_get_violation_by_id(mocker, session_fixture):
    client = session_fixture
    mocker.patch.object(client, "get_violation_by_id", return_value=Parser(chunk=VIOLATION_BY_ID_RAW_JSON, keys=[], iocs_keys=[]))
    response = client.get_formatted_violation_by_id(violation_id=TEST_VIOLATION_ID, get_images=False)
    assert isinstance(response[0], dict)


def test_get_file(mocker, session_fixture):
    client = session_fixture
    mocker.patch.object(client, "get_file", return_value=[TEST_GET_FILES_BYTES, "image/jpeg"])
    response = client.get_file(file_sha=TEST_FILE_SHA)
    assert isinstance(response[0], bytes)


""" Commands Testins """


def test_get_avalible_commands():
    response_commands = GroupIBDigitalRiskProtection.Commands.get_avalible_commands()

    required_commands = [cmd.value for cmd in Commands]
    assert len(required_commands) == len(response_commands)
    assert "gibdrp" in str(response_commands)


def test_command_get_brands(mocker, session_fixture):
    client = session_fixture
    mocker.patch.object(client, "get_formatted_brands", return_value=[BRANDS_RAW_JSON])
    result, requested_method = GroupIBDigitalRiskProtection.Commands(
        client=client,
        command=Commands.GET_BRANDS.value,
        args={},
        first_fetch="",
        max_requests=1,
        download_images=False,
        violation_subtypes=[],
        only_typosquatting=False,
    ).get_results()
    assert result is not None
    assert isinstance(requested_method, str)
    assert requested_method in GroupIBDigitalRiskProtection.Commands.methods_requiring_return_results


def test_command_get_subscriptions(mocker, session_fixture):
    client = session_fixture
    mocker.patch.object(client, "get_formatted_subscriptions", return_value=[SUBSCRIPTIONS_RAW_JSON])
    result, requested_method = GroupIBDigitalRiskProtection.Commands(
        client=client,
        command=Commands.GET_SUBSCRIPTIONS.value,
        args={},
        first_fetch="",
        max_requests=1,
        download_images=False,
        violation_subtypes=[],
        only_typosquatting=False,
    ).get_results()
    assert result is not None
    assert isinstance(requested_method, str)
    assert requested_method in GroupIBDigitalRiskProtection.Commands.methods_requiring_return_results


def test_command_get_violation_by_id(mocker, session_fixture):
    client = session_fixture
    mocker.patch.object(client, "get_violation_by_id", return_value=Parser(chunk=VIOLATION_BY_ID_RAW_JSON, keys=[], iocs_keys=[]))
    result, requested_method = GroupIBDigitalRiskProtection.Commands(
        client=client,
        command=Commands.GET_VIOLATION_BY_ID.value,
        args={"id": TEST_VIOLATION_ID},
        first_fetch="",
        max_requests=1,
        download_images=False,
        violation_subtypes=[],
        only_typosquatting=False,
    ).get_results()
    assert isinstance(result, list)
    first = result[0]
    assert first is not None
    assert isinstance(requested_method, str)
    assert requested_method in GroupIBDigitalRiskProtection.Commands.methods_requiring_return_results


def test_command_test_module(mocker, session_fixture):
    client = session_fixture
    mocker.patch.object(client, "get_formatted_brands", return_value=[BRANDS_RAW_JSON])
    result, requested_method = GroupIBDigitalRiskProtection.Commands(
        client=client,
        command=Commands.TEST_MODULE.value,
        args={},
        first_fetch="",
        max_requests=1,
        download_images=False,
        violation_subtypes=[],
        only_typosquatting=False,
    ).get_results()
    assert isinstance(result, str)
    assert result == "ok"
    assert isinstance(requested_method, str)
    assert requested_method in GroupIBDigitalRiskProtection.Commands.methods_requiring_return_results


def test_client_get_violation_section_number_and_error(session_fixture):
    client = session_fixture
    assert client._get_violation_section_number("Web") == 1
    assert client._get_violation_section_number("Social Networks") == 4
    with pytest.raises(ValueError):
        client._get_violation_section_number("Unknown Section")


def test_client_generate_seq_update_calls_poller(session_fixture, mocker):
    client = session_fixture

    class PollerStub:
        def get_seq_update_dict(self, date, collection):
            # With `collection` provided, DRPPoller.get_seq_update_dict is expected to return int seqUpdate.
            return 123

    client.poller = PollerStub()
    seq = client.generate_seq_update("2024-01-15")
    assert seq == 123

    # invalid date -> DemistoException
    with pytest.raises(GroupIBDigitalRiskProtection.DemistoException):
        client.generate_seq_update("not-a-date")


def test_commonhelpers_convert_iso8601_with_timezone_and_invalid():
    s = "2024-10-30T15:12:34+0000"
    out = GroupIBDigitalRiskProtection.CommonHelpers.convert_iso8601_with_timezone(s)
    assert out == "2024-10-30T15:12:34+00:00"
    with pytest.raises(ValueError):
        GroupIBDigitalRiskProtection.CommonHelpers.convert_iso8601_with_timezone("bad")


def test_commonhelpers_format_dates_in_dict():
    data = {
        "first_detected": "2024-10-30T15:12:34+0000",
        "stages": ["2024-10-30T15:12:34+0000", "2024-10-30T16:12:34+0000"],
        "nested": {"dates_created_date": "2024-10-30T15:12:34+0000"},
    }
    out = GroupIBDigitalRiskProtection.CommonHelpers.format_dates_in_dict(data)
    assert out["first_detected"].endswith("+00:00")
    assert all(x.endswith("+00:00") for x in out["stages"])
    # Nested conversion only happens when the key itself is one of date_keys; convert nested dict explicitly.
    converted_nested = GroupIBDigitalRiskProtection.CommonHelpers.format_dates_in_dict(out["nested"])
    assert converted_nested["dates_created_date"].endswith("+00:00")


def test_commonhelpers_replace_empty_values_and_keys_cleanup():
    d = {"a": "", "b": [], "c": ["ok"], "d": [[], []]}
    out = GroupIBDigitalRiskProtection.CommonHelpers.replace_empty_values(d)
    assert isinstance(out, dict)
    out_dict = out if isinstance(out, dict) else {}
    assert out_dict.get("a") is None
    assert out_dict.get("b") is None
    assert out_dict.get("c") == ["ok"]
    assert out_dict.get("d") is None

    lst = [{"A_B": 1, "Some_Key": 2}]
    out2 = GroupIBDigitalRiskProtection.CommonHelpers.remove_underscore_and_lowercase_keys(lst)
    assert out2 == [{"ab": 1, "somekey": 2}]


def test_commonhelpers_data_pre_cleaning_and_source_mapping():
    feed = {"violation_uri": "//example.com", "tags": [None, "a"], "source": 1}
    feed = GroupIBDigitalRiskProtection.CommonHelpers.data_pre_cleaning(feed)
    assert feed["violation_uri"] == "example.com"
    assert feed["tags"] == ["a"]
    feed = GroupIBDigitalRiskProtection.CommonHelpers.violation_source_mapping(feed)
    assert feed["source"] == "WEB"


def test_commonhelpers_extract_mime_type():
    mt = GroupIBDigitalRiskProtection.CommonHelpers.extract_mime_type("image/png; charset=UTF-8")
    assert mt == "image/png"
    mt = GroupIBDigitalRiskProtection.CommonHelpers.extract_mime_type("")
    assert mt == "image/jpeg"


def test_incident_builder_transform_fields_to_grid_table(session_fixture):
    client = session_fixture
    builder = GroupIBDigitalRiskProtection.IncidentBuilder(
        client=client,
        last_run={},
        first_fetch_time="3 days",
        max_requests=1,
        download_images=False,
        only_typosquatting=False,
        violation_subtypes=None,
        violation_section=None,
        brands=None,
    )
    incident = {
        "scores": {
            "type": ["risk", "position"],
            "score": [10, 99],
            "version": [1, 2],
        }
    }
    out = builder.transform_fields_to_grid_table(incident)
    assert isinstance(out["scores"], list)
    assert all(entry["type"] != "position" for entry in out["scores"])


def test_commands_fetch_incidents_flow(session_fixture):
    client = session_fixture

    class Portion:
        sequpdate = {"cursor": "abc"}

        def parse_portion(self, keys, as_json=False):  # noqa: ARG002
            return [
                {
                    "id": "1",
                    "title": "t",
                    "violation_uri": "//example.com",
                    "source": 1,
                    "tags": [None, "x"],
                    "dates_created_date": "2024-10-30T15:12:34+0000",
                    "images": [],
                    "scores": {"type": ["risk", "position"], "score": [10, 99], "version": [1, 2]},
                }
            ]

    def generator():
        yield Portion()

    # override instance method with lightweight fake
    client.create_generator = lambda **kwargs: generator()  # type: ignore

    result, requested_method = GroupIBDigitalRiskProtection.Commands(
        client=client,
        command=Commands.FETCH_INCIDENTS.value,
        args={},
        first_fetch="3 days",
        max_requests=1,
        download_images=False,
        violation_subtypes=None,
        only_typosquatting=False,
    ).get_results()

    assert isinstance(result, tuple)
    next_run, incidents = result
    assert isinstance(next_run, dict)
    assert "last_fetch" in next_run
    assert isinstance(incidents, list)
    assert len(incidents) == 1
    assert requested_method == "fetch_incidents"


def test_client_change_violation_status_paths(mocker, session_fixture):
    client = session_fixture

    class Resp:
        def __init__(self, raw_dict):
            self.raw_dict = raw_dict

    # perform POST path
    mocker.patch.object(
        client.poller,
        "search_feed_by_id",
        return_value=Resp({"violation": {"status": "detected", "approveState": "under_review"}}),
    )

    class HTTPResponse:
        status_code = 200

    mocker.patch.object(client, "_http_request", return_value=HTTPResponse())
    assert client.change_violation_status(feed_id="1", status="approve") == 200

    # cannot change path
    mocker.patch.object(
        client.poller, "search_feed_by_id", return_value=Resp({"violation": {"status": "approved", "approveState": "approved"}})
    )
    res = client.change_violation_status(feed_id="1", status="approve")
    assert isinstance(res, str)
    assert "Can not change" in res


def test_client_get_formatted_violation_by_id_with_images(session_fixture, mocker):
    client = session_fixture

    class FakeParser:
        def parse_portion(self, keys, as_json=False):  # noqa: ARG002
            return [{"id": "1", "violation_uri": "//example.com", "source": 1, "images": ["abc"]}]

    mocker.patch.object(client, "get_violation_by_id", return_value=FakeParser())
    mocker.patch.object(client, "get_file", return_value=(b"bytes", "image/jpeg"))
    parsed, images = client.get_formatted_violation_by_id(violation_id="1", get_images=True)
    assert isinstance(parsed, dict)
    assert isinstance(images, list)
    assert len(images) > 0
    first = images[0]
    assert isinstance(first, dict)
    assert first.get("mime_type") == "image/jpeg"


def test_commonhelpers_get_table_data_returns_additional_tables():
    feed = {"scores": {"type": ["risk", "position"], "score": [10, 99], "version": [1, 2]}}
    updated_feed, additional_tables = GroupIBDigitalRiskProtection.CommonHelpers.get_table_data(feed)
    assert isinstance(updated_feed, dict)
    assert isinstance(additional_tables, list)
    assert len(additional_tables) > 0


def test_commands_change_violation_status_message(session_fixture, mocker):
    client = session_fixture
    mocker.patch.object(client, "change_violation_status", return_value=200)
    res, requested_method = GroupIBDigitalRiskProtection.Commands(
        client=client,
        command=Commands.CHANGE_VIOLATION_STATUS.value,
        args={"id": "1", "status": "approve"},
        first_fetch="",
        max_requests=1,
        download_images=False,
        violation_subtypes=None,
        only_typosquatting=False,
    ).get_results()
    assert isinstance(res, str)
    assert "Request to change violation status sent" in res
    assert requested_method == "change_violation_status"


def test_get_formatted_brands_and_subscriptions_no_methods(session_fixture):
    client = session_fixture

    class Dummy:
        pass

    client.poller = Dummy()  # type: ignore
    assert client.get_formatted_brands() == []
    assert client.get_formatted_subscriptions() == []