ReliaQuest GreyMatter DRP Event Collector

ReliaQuest GreyMatter DRP Event Collector monitors and manages an organization's digital risk across the widest range of data sources within the open, deep, and dark web.

Data Enrichment & Threat Intelligence · ReliaQuest Digital Risk Protection

Details

IDReliaQuest GreyMatter DRP Event Collector
ProviderReliaQuest
CategoryData Enrichment & Threat Intelligence
From Version8.0.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

ReliaQuest GreyMatter DRP Event Collector minimizes digital risk by identifying unwanted exposure and protecting against external threats. The award-winning SearchLight solution provides ongoing monitoring of a customer’s unique assets and exposure across the open, deep, and dark web. This enables clients to detect data loss, brand impersonation, infrastructure risks, cyber threats, and much more.

This integration fetches event items which can be either incident/alerts, for more information refer here

This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.

Configure ReliaQuest GreyMatter DRP Event Collector On XSIAM

  1. Navigate to Settings > Configurations > Data Collection > Automations & Feed Integrations.
  2. Search for Relia Quest GreyMatter DRP Event Collector.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required
    Server URL URL for the Relia Quest API instance. True
    Account ID The account ID for the Reila Quest instance. True
    Maximum number of events per fetch The maximum number of events to fetch every time fetch is executed. Default is 1000. True
    Trust any certificate (not secure)   False
    Use system proxy settings   False
    Username The username to authenticate Relia Quest Event Collector. False
    Password The password to authenticate Relia Quest Event Collector False
  4. Click Test to validate the URLs, token, and connection.

ReliaQuest GreyMatter DRP EventCollector Authentication

Requests to all operation endpoints require HTTP Basic authentication, using dedicated (high entropy) API credentials. These normally consist of a six character key, and a 32 character ‘secret’. Note that you will not be able to use your normal email/password login details with the HTTP Basic authentication mechanism.

Contact your Digital Shadows representative to obtain API credentials.

To authenticate the integration, you must have a username, password and account ID. To get the account ID, see here.

Limitations

Increasing the Maximum number of events per fetch parameter to high numbers can cause rate-limits, however The integration will recover from those rate-limits automatically. For more information about rate-limits, see here.

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.

relia-quest-get-events


Manual command to fetch reila-quest events and display them.

Base Command

relia-quest-get-events

Input

Argument Name Description Required
start_time From which time to get the events in ISO8601 format, for example 2020-09-24T16:30:10.016Z or (<number> <time unit>, e.g., 12 hours, 7 days). If not provided, will retrieve the oldest events available in case event_num_after argument is not provided. Default is 3 days ago. Optional
end_time Until which time to get the events in ISO8601 format, for example 2020-09-24T16:30:10.016Z or (<number> <time unit>, e.g., 12 hours, 7 days). Optional
limit The maximum number of events to retrieve. Default is 200. Optional
event_num_after Fetch events that were created after a specific event-number. Optional

Context Output

Path Type Description
ReilaQuest.Events Unknown A list of events.

Configuration parameters

  • url — Server URL (required)
  • credentials — Username (required)
  • account_id — Account ID (required)
  • max_fetch_events — Maximum number of events per fetch (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • relia-quest-get-events

    Manual command to fetch reila-quest events and display them.

import hashlib
import json

import pytest
from CommonServerPython import *
from ReliaQuestGreyMatterDRPEventCollector import LAST_FETCHED_EVENT_NUM, RATE_LIMIT_LAST_RUN, RateLimitError, ReilaQuestClient

TEST_URL = "https://test.com/api"


@pytest.fixture()
def client() -> ReilaQuestClient:
    return ReilaQuestClient(
        url=TEST_URL,
        account_id="1234",
        username="test",
        password="test",
    )


class HttpRequestMock:
    LAST_EVENT_TIME_AND_NUM = None

    def __init__(self, num_of_events: int, num_of_alerts: int, num_of_incidents: int):
        self.num_of_events = num_of_events
        self.num_of_fetched_events = 0
        self.num_of_alerts = num_of_alerts
        self.num_of_fetched_alerts = 0
        self.num_of_incidents = num_of_incidents
        self.num_of_fetched_incidents = 0

    def http_request_side_effect(self, method: str, url_suffix: str, params: Dict | None = None, **kwargs):
        if url_suffix == "/triage-item-events":
            if self.num_of_fetched_events >= self.num_of_events:
                return create_mocked_response([])
            limit = params["limit"]
            event_num_after = params["event-num-after"]
            if event_num_after:
                event_num_after += 1
            response = create_triage_item_events(limit, start_event_num=event_num_after or 1)
            self.num_of_fetched_events += len(response)
            if self.num_of_fetched_events > self.num_of_events:
                response = response[: self.num_of_fetched_events - self.num_of_events]

        elif url_suffix == "/triage-items":
            triage_item_ids = params["id"]
            triage_item_alerts = triage_item_ids[: self.num_of_alerts - self.num_of_fetched_alerts]
            alerts_response = create_triage_items_from_events(triage_item_alerts, item_type="alert-id")
            triage_item_incidents = triage_item_ids[self.num_of_alerts - self.num_of_fetched_alerts :]
            incidents_response = create_triage_items_from_events(triage_item_incidents, item_type="incident-id")

            self.num_of_fetched_alerts += len(alerts_response)
            self.num_of_fetched_incidents += len(incidents_response)

            response = alerts_response + incidents_response

        elif url_suffix == "/alerts":
            response = create_incidents_and_alerts_from_triaged_items(params["id"], item_type="alert-id", amount_of_assets=1)

        elif url_suffix == "/incidents":
            response = create_incidents_and_alerts_from_triaged_items(params["id"], item_type="incident-id", amount_of_assets=1)

        elif url_suffix == "/assets":
            response = create_assets(params["id"])

        else:
            response = []

        return create_mocked_response(response)


def create_mocked_response(response: List[Dict] | Dict, status_code: int = 200) -> requests.Response:
    mocked_response = requests.Response()
    mocked_response._content = json.dumps(response).encode("utf-8")
    mocked_response.status_code = status_code
    return mocked_response


def create_triage_item_events(num_of_events: int, start_event_num: int = 1) -> List[Dict]:
    return [
        {
            "event-num": event_num,
            "event-created": "2020-09-24T16:30:10.016Z",
            "triage-item-id": event_num,
        }
        for event_num in range(start_event_num, num_of_events + start_event_num)
    ]


def create_triage_items_from_events(triage_item_ids: List[str], item_type: str) -> List[Dict]:
    if item_type not in {"incident-id", "alert-id"}:
        raise ValueError(f"item-type {item_type} must be one of incident-id/alert-id")

    return [{"id": triage_item_id, "title": "title", "source": {item_type: triage_item_id}} for triage_item_id in triage_item_ids]


def create_incidents_and_alerts_from_triaged_items(_ids: List[str], item_type: str, amount_of_assets: int = 0) -> List[Dict]:
    if item_type not in {"incident-id", "alert-id"}:
        raise ValueError(f"item-type {item_type} must be one of incident-id/alert-id")
    events = []
    for _id in _ids:
        events.append({"id": _id, "title": f"{item_type}-{_id}", "assets": []})

    if amount_of_assets > 0:
        for i, event in enumerate(events):
            event_copy = event.copy()
            event_copy["unique_id"] = i
            event["assets"].append({"id": hashlib.sha256(json.dumps(event_copy, sort_keys=True).encode()).hexdigest()})

    return events


def create_assets(asset_ids: List[str]) -> List[Dict]:
    return [{"id": _id, "type": f"asset-{_id}"} for _id in asset_ids]


def test_the_test_module(requests_mock, client: ReilaQuestClient):
    """
    Given:
     - a single event
     - api returns 200 ok

    When:
     - running test-module

    Then:
     - make sure the test is successful.
    """
    from ReliaQuestGreyMatterDRPEventCollector import test_module

    requests_mock.get(f"{TEST_URL}/triage-item-events?limit=1", json=create_triage_item_events(num_of_events=1))
    assert test_module(client) == "ok"


def test_http_request_rate_limit(mocker, client: ReilaQuestClient):
    """
    Given:
     - api rate limit reached
     - api informs to try a resend the request after a milisecond
     - second api response 200 ok

    When:
     - running http_request

    Then:
     - make sure the response is returned properly
    """
    mocked_responses = [
        create_mocked_response({"retry-after": "2020-09-24T16:30:10.017Z"}, status_code=429),
    ]
    mocker.patch.object(client, "_http_request", side_effect=mocked_responses)
    with pytest.raises(RateLimitError):
        client.http_request("suffix")


def test_http_request_connection_errors(mocker, client: ReilaQuestClient):
    """
    Given:
     - connection error exceptions
     - request that succeeded after connection errors

    When:
     - running http_request

    Then:
     - make sure the retry mechanism is triggered and the response is returned properly
    """
    from requests.exceptions import ConnectionError, Timeout

    sleep_mocker = mocker.patch("CommonServerPython.time.sleep")
    mocked_responses = [Timeout, ConnectionError, create_mocked_response(response={"test": "test"})]
    mocker.patch.object(client, "_http_request", side_effect=mocked_responses)
    assert client.http_request("suffix") == {"test": "test"}
    assert sleep_mocker.called


@pytest.mark.parametrize(
    "limit, num_of_events",
    [(200, 1000), (1500, 1000), (100, 100), (10000, 15000)],
)
def test_list_triage_item_events(client: ReilaQuestClient, mocker, limit: int, num_of_events: int):
    """
    Given:
     - maximum limit & actual number of events exist in the api

    When:
     - running list_triage_item_events

    Then:
     - make sure the right amount of events is returned
    """
    http_mocker = HttpRequestMock(num_of_events, num_of_alerts=0, num_of_incidents=0)

    mocker.patch.object(client, "_http_request", side_effect=http_mocker.http_request_side_effect)
    fetched_events = []
    for events, latest_event in client.list_triage_item_events(limit=limit):
        fetched_events.extend(events)
        assert latest_event == len(fetched_events)

    assert len(fetched_events) == min(num_of_events, limit)


class TestFetchEvents:
    def test_fetch_events_no_last_run_single_iteration_sanity_test(self, mocker):
        """
        Given:
         - 100 events
         - no last run

        When:
         - running the entire fetch-events flow

        Then:
         - make sure the events are enriched as expected
         - make sure all the 100 events are fetched
        """
        import ReliaQuestGreyMatterDRPEventCollector

        send_events_mocker = mocker.patch.object(ReliaQuestGreyMatterDRPEventCollector, "send_events_to_xsiam")
        set_last_run_mocker = mocker.patch.object(demisto, "setLastRun", return_value={})
        mocker.patch.object(demisto, "getLastRun", return_value={})
        mocker.patch.object(
            demisto,
            "params",
            return_value={
                "url": TEST_URL,
                "credentials": {
                    "identifier": "1234",
                    "password": "1234",
                },
                "max_fetch_events": 200,
            },
        )
        mocker.patch.object(demisto, "command", return_value="fetch-events")

        http_mocker = HttpRequestMock(100, num_of_alerts=50, num_of_incidents=50)

        mocker.patch.object(
            ReliaQuestGreyMatterDRPEventCollector.ReilaQuestClient,
            "_http_request",
            side_effect=http_mocker.http_request_side_effect,
        )

        ReliaQuestGreyMatterDRPEventCollector.main()
        assert send_events_mocker.call_count == 1
        events = send_events_mocker.call_args[0][0]
        assert len(events) == 100

        assert set_last_run_mocker.call_args[0][0][LAST_FETCHED_EVENT_NUM] == 100
        for event in events[0:50]:
            assert event["triage-item"]
            assert event["alert"]
            assert event["assets"]

        for event in events[50:100]:
            assert event["triage-item"]
            assert event["incident"]
            assert event["assets"]

    def test_fetch_events_sanity_rate_limit_error(self, mocker):
        """
        Given:
         - api rate limit error

        When:
         - running the entire fetch-events flow

        Then:
         - make sure all the last run saves the retry-after
        """
        import ReliaQuestGreyMatterDRPEventCollector

        send_events_mocker = mocker.patch.object(ReliaQuestGreyMatterDRPEventCollector, "send_events_to_xsiam")
        set_last_run_mocker = mocker.patch.object(demisto, "setLastRun", return_value={})
        mocker.patch.object(demisto, "error")
        mocker.patch.object(demisto, "getLastRun", return_value={})
        mocker.patch.object(
            demisto,
            "params",
            return_value={
                "url": TEST_URL,
                "credentials": {
                    "identifier": "1234",
                    "password": "1234",
                },
                "max_fetch_events": 200,
            },
        )
        mocker.patch.object(demisto, "command", return_value="fetch-events")
        mocker.patch.object(
            ReliaQuestGreyMatterDRPEventCollector.ReilaQuestClient,
            "_http_request",
            side_effect=[
                create_mocked_response(response={"retry-after": "2024-01-18T10:22:00Z"}, status_code=429),
            ],
        )
        ReliaQuestGreyMatterDRPEventCollector.main()
        assert send_events_mocker.call_count == 0
        assert set_last_run_mocker.call_args[0][0][RATE_LIMIT_LAST_RUN] == "2024-01-18T10:22:00Z"

    def test_fetch_events_no_events(self, mocker):
        """
        Given:
         - flow where there is no last run or no events

        When:
         - running the entire fetch-events flow

        Then:
         - make sure that there aren't any events fetched
        """
        import ReliaQuestGreyMatterDRPEventCollector

        send_events_mocker = mocker.patch.object(ReliaQuestGreyMatterDRPEventCollector, "send_events_to_xsiam")
        mocker.patch.object(demisto, "getLastRun", return_value={})
        mocker.patch.object(
            demisto,
            "params",
            return_value={
                "url": TEST_URL,
                "credentials": {
                    "identifier": "1234",
                    "password": "1234",
                },
                "max_fetch_events": 200,
            },
        )
        mocker.patch.object(demisto, "command", return_value="fetch-events")
        mocker.patch.object(
            ReliaQuestGreyMatterDRPEventCollector.ReilaQuestClient,
            "_http_request",
            side_effect=[
                create_mocked_response(response=[]),
            ],
        )

        ReliaQuestGreyMatterDRPEventCollector.main()
        assert send_events_mocker.call_count == 0

    def test_fetch_events_no_events_with_last_run(self, mocker):
        """
        Given:
         - flow where there is last run from previous fetch, but no more events in the api

        When:
         - running the entire fetch-events flow

        Then:
         - make sure that there aren't any events fetched and the last run is kept the same
        """
        import ReliaQuestGreyMatterDRPEventCollector

        send_events_mocker = mocker.patch.object(ReliaQuestGreyMatterDRPEventCollector, "send_events_to_xsiam")
        set_last_run_mocker = mocker.patch.object(demisto, "setLastRun")
        mocker.patch.object(demisto, "error")
        mocker.patch.object(demisto, "getLastRun", return_value={LAST_FETCHED_EVENT_NUM: 1})
        mocker.patch.object(
            demisto,
            "params",
            return_value={
                "url": TEST_URL,
                "credentials": {
                    "identifier": "1234",
                    "password": "1234",
                },
                "max_fetch_events": 200,
            },
        )
        mocker.patch.object(demisto, "command", return_value="fetch-events")
        http_mocker = HttpRequestMock(0, num_of_alerts=0, num_of_incidents=0)
        mocker.patch.object(
            ReliaQuestGreyMatterDRPEventCollector.ReilaQuestClient,
            "_http_request",
            side_effect=http_mocker.http_request_side_effect,
        )

        ReliaQuestGreyMatterDRPEventCollector.main()
        assert send_events_mocker.call_count == 0
        assert set_last_run_mocker.call_args[0][0] == {LAST_FETCHED_EVENT_NUM: 1}

    def test_fetch_events_multiple_events_no_rate_limits_no_last_run(self, mocker):
        """
        Given:
         - flow where there are multiple events and no last run

        When:
         - running the entire fetch-events flow

        Then:
         - make sure that all events are enriched and fetched, make sure the send_events_to_xsiam is called multiple times
         - make sure last run saves the largest event number
        """
        from unittest.mock import MagicMock

        import ReliaQuestGreyMatterDRPEventCollector

        send_events_mocker: MagicMock = mocker.patch.object(ReliaQuestGreyMatterDRPEventCollector, "send_events_to_xsiam")
        set_last_run_mocker: MagicMock = mocker.patch.object(demisto, "setLastRun", return_value={})
        mocker.patch.object(demisto, "getLastRun", return_value={})
        mocker.patch.object(
            demisto,
            "params",
            return_value={
                "url": TEST_URL,
                "credentials": {
                    "identifier": "1234",
                    "password": "1234",
                },
                "max_fetch_events": 4000,
            },
        )
        mocker.patch.object(demisto, "command", return_value="fetch-events")

        http_mocker = HttpRequestMock(3500, num_of_alerts=1750, num_of_incidents=1750)

        mocker.patch.object(
            ReliaQuestGreyMatterDRPEventCollector.ReilaQuestClient,
            "_http_request",
            side_effect=http_mocker.http_request_side_effect,
        )

        ReliaQuestGreyMatterDRPEventCollector.main()

        assert send_events_mocker.call_count == 4
        fetched_events = []
        for call in send_events_mocker.call_args_list:
            fetched_events.extend(call.args[0])

        assert len(fetched_events) == 3500

        assert set_last_run_mocker.call_args[0][0][LAST_FETCHED_EVENT_NUM] == 3500
        for event in fetched_events[0:1750]:
            assert event["triage-item"]
            assert event["alert"]
            assert event["assets"]

        for event in fetched_events[1750:3500]:
            assert event["triage-item"]
            assert event["incident"]
            assert event["assets"]

    def test_fetch_events_multiple_events_no_rate_limits_with_last_run(self, mocker):
        """
        Given:
         - flow where there are multiple events and last run indicates on latest event

        When:
         - running the entire fetch-events flow

        Then:
         - make sure that all events are enriched and fetched, make sure the send_events_to_xsiam is called multiple times
         - make sure that the latest event-num is now larger = 10000
        """
        from unittest.mock import MagicMock

        import ReliaQuestGreyMatterDRPEventCollector

        send_events_mocker: MagicMock = mocker.patch.object(ReliaQuestGreyMatterDRPEventCollector, "send_events_to_xsiam")
        set_last_run_mocker: MagicMock = mocker.patch.object(demisto, "setLastRun", return_value={})
        mocker.patch.object(demisto, "getLastRun", return_value={LAST_FETCHED_EVENT_NUM: 5000})
        mocker.patch.object(
            demisto,
            "params",
            return_value={
                "url": TEST_URL,
                "credentials": {
                    "identifier": "1234",
                    "password": "1234",
                },
                "max_fetch_events": 10000,
            },
        )
        mocker.patch.object(demisto, "command", return_value="fetch-events")

        http_mocker = HttpRequestMock(5000, num_of_alerts=2500, num_of_incidents=2500)

        mocker.patch.object(
            ReliaQuestGreyMatterDRPEventCollector.ReilaQuestClient,
            "_http_request",
            side_effect=http_mocker.http_request_side_effect,
        )

        ReliaQuestGreyMatterDRPEventCollector.main()

        assert send_events_mocker.call_count == 5
        fetched_events = []
        for call in send_events_mocker.call_args_list:
            fetched_events.extend(call.args[0])

        assert len(fetched_events) == 5000

        assert set_last_run_mocker.call_args[0][0][LAST_FETCHED_EVENT_NUM] == 10000
        for event in fetched_events[0:2500]:
            assert event["triage-item"]
            assert event["alert"]
            assert event["assets"]

        for event in fetched_events[2500:5000]:
            assert event["triage-item"]
            assert event["incident"]
            assert event["assets"]


def test_get_events_command(mocker, client: ReilaQuestClient):
    """
    Given:
     - 5000 events

    When:
     - running the get_events_command

    Then:
     - make sure that all events are enriched and fetched (5000)
    """
    from ReliaQuestGreyMatterDRPEventCollector import get_events_command

    http_mocker = HttpRequestMock(5000, num_of_alerts=2500, num_of_incidents=2500)
    mocker.patch.object(client, "_http_request", side_effect=http_mocker.http_request_side_effect)

    command_results = get_events_command(client, args={"limit": 10000})
    events = command_results.outputs
    assert len(events) == 5000

    for event in events[0:2500]:
        assert event["triage-item"]
        assert event["alert"]
        assert event["assets"]

    for event in events[2500:5000]:
        assert event["triage-item"]
        assert event["incident"]
        assert event["assets"]