DecyfirEventCollector

Collects event logs from DeCYFIR for ingestion into Cortex XSIAM.

Analytics & SIEM · DeCYFIR

Details

IDDecyfirEventCollector
ProviderCYFIRMA
CategoryAnalytics & SIEM
From Version8.4.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Collects event logs from DeCYFIR for ingestion into Cortex XSIAM.

Once configured, the integration periodically fetches event logs from DeCYFIR’s APIs and sends them to Cortex XSIAM for ingestion and analysis.

  • Events are fetched in real time (starting from the moment the integration is enabled).
  • Each event type (Access Logs, Assets Logs, Digital Risk Keywords Logs) is fetched separately using its own pagination and limit.
  • The integration automatically tracks and stores the last fetched timestamp and event IDs to prevent duplication.

Configure DeCYFIR Event Collector in Cortex

Parameter Required
Server URL True
API Key True
Event types to fetch True
Maximum number of Access Logs events per fetch False
Maximum number of Assets Logs events per fetch False
Maximum number of Digital Risk Keywords Logs events per fetch False
Trust any certificate (not secure) False
Use system proxy settings False

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.

decyfir-get-events


Retrieve Decyfir events manually. This command is used for developing/ debugging and is to be used with caution, as it can create events, leading to events duplication and exceeding the API request limitation.

Base Command

decyfir-get-events

Input

Argument Name Description Required
event_types Comma-separated list of event types to fetch. Possible values are: Access Logs, Assets Logs, Digital Risk Keywords Logs. Default is Access Logs,Assets Logs,Digital Risk Keywords Logs. Required
should_push_events Set this argument to True to send the fetched events to Cortex XSIAM. If False, the command will only display them in the War Room.
. Possible values are: True, False. Default is False.
Required
from_date Fetch events created after the specified time (e.g., “12 hours”, “7 days”). If not provided, defaults to “3 months”.
. Default is 3 months.
Optional

Context Output

There is no context output for this command.

Configuration parameters

  • url — Server URL (required)
  • credentials — (required)
  • event_types_to_fetch — Event types to fetch (required)
  • max_access_logs_events_per_fetch — Maximum number of Access Logs events per fetch
  • max_assets_logs_events_per_fetch — Maximum number of Assets Logs events per fetch
  • max_drkl_events_per_fetch — Maximum number of Digital Risk Keywords Logs events per fetch
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • decyfir-get-events

    Manual command to fetch events. This command is used for developing/debugging and is to be used with caution, as it can create events, leading to events duplication and API request limitation exceeding.

import pytest
from datetime import datetime, UTC

from CommonServerPython import arg_to_datetime
from DecyfirEventCollector import (
    Client,
    ACCESS_LOGS,
    DRK_LOGS,
    get_timestamp_from_datetime,
    extract_event_time,
    add_event_fields,
    remove_duplicate_logs,
    update_fetched_event_ids,
    compute_next_fetch_time,
    fetch_events,
    test_module,
    get_events_command,
)
import demistomock as demisto


@pytest.fixture(autouse=True)
def mock_demisto(monkeypatch):
    """Mock demisto functions and disable real network/debug actions."""
    monkeypatch.setattr(demisto, "debug", lambda *_, **__: None)
    monkeypatch.setattr("DecyfirEventCollector.return_results", lambda *_, **__: None)
    monkeypatch.setattr("DecyfirEventCollector.return_error", lambda x: (_ for _ in ()).throw(Exception(x)))
    monkeypatch.setattr("DecyfirEventCollector.send_events_to_xsiam", lambda *_, **__: None)


@pytest.fixture
def client(monkeypatch):
    """Create a fake Decyfir client returning mock Access Log events."""
    c = Client(base_url="https://test.com", verify=False, proxy=False, api_key="abc123")
    monkeypatch.setattr(
        c,
        "_http_request",
        lambda **_: [
            {
                "uid": "8f297095-9515-3f37-82fc-296a99fb8753",
                "event_date": "2025-11-06T04:24:12.000+00:00",
                "event_type": "AUTHENTICATION_ATTEMPT",
                "ip": "1.2.3.4",
                "principal": "test@gmail.com",
            }
        ],
    )
    return c


@pytest.fixture
def drk_event():
    """Fixture providing a realistic DRK event sample."""
    return {
        "event_action": "ORG_ASSETS_CREATE",
        "asset_comments": "",
        "asset_name": "test_name",
        "vendor": "",
        "asset_type": "LinkedIn",
        "modified_by": "xx@test.com",
        "created_date": "2025-06-10T13:18:35.001",
        "modified_date": "2025-06-10T13:18:35.001",
        "version": "",
        "created_by": "",
    }


@pytest.fixture
def access_event():
    """Fixture providing a realistic Access Log event sample."""
    return {
        "principal": "test@gmail.com",
        "uid": "8f297095-9515-3f37-82fc-296a99fb8753",
        "event_type": "AUTHENTICATION_ATTEMPT",
        "ip": "1.2.3.4",
        "event_date": "2025-11-06T04:24:12.000+00:00",
        "name": "client_ip",
    }


# --- Utility Tests ---


def test_get_timestamp_from_datetime_rounding():
    """
    GIVEN a datetime with fractional seconds
    WHEN get_timestamp_from_datetime is called
    THEN Access Logs timestamps are rounded to seconds
         and DRK logs retain millisecond precision.
    """
    dt = datetime(2025, 11, 6, 4, 24, 12, 500_000, tzinfo=UTC)
    ts_access = get_timestamp_from_datetime(dt, ACCESS_LOGS)
    ts_drk = get_timestamp_from_datetime(dt, DRK_LOGS)
    assert ts_access % 1000 == 0
    assert ts_drk % 1000 != 0


# --- Event Extraction & Enrichment ---


def test_extract_event_time_access(access_event):
    """
    GIVEN an Access Log event with event_date field
    WHEN extract_event_time is called
    THEN a timezone-aware datetime is returned with correct hour and minute.
    """
    dt = extract_event_time(access_event, ACCESS_LOGS)
    assert isinstance(dt, datetime)
    assert dt.tzinfo == UTC
    assert dt.hour == 4
    assert dt.minute == 24


def test_extract_event_time_drk(drk_event):
    """
    GIVEN a DRK log event with created_date field
    WHEN extract_event_time is called
    THEN it returns the proper UTC datetime object from created_date.
    """
    dt = extract_event_time(drk_event, DRK_LOGS)
    assert dt.year == 2025
    assert dt.month == 6
    assert dt.second == 35


def test_add_event_fields_access(access_event):
    """
    GIVEN an Access Log event
    WHEN add_event_fields is applied
    THEN _time and source_log_type are added,
         and _ENTRY_STATUS is not present (not applicable).
    """
    add_event_fields([access_event], ACCESS_LOGS)
    assert access_event["_time"].startswith("2025-11-06T04:24:12")
    assert access_event["source_log_type"] == "access_logs"
    assert "_ENTRY_STATUS" not in access_event


def test_add_event_fields_drk(drk_event):
    """
    GIVEN a DRK event where created_date == modified_date
    WHEN add_event_fields is applied
    THEN the event includes _time, source_log_type, and _ENTRY_STATUS='new'.
    """
    add_event_fields([drk_event], DRK_LOGS)
    assert drk_event["_time"].startswith("2025-06-10T13:18:35")
    assert drk_event["source_log_type"] == "dr_keywords_logs"
    assert drk_event["_ENTRY_STATUS"] == "new"


# --- Deduplication & ID Tracking ---


def test_remove_duplicate_logs():
    """
    GIVEN logs with previously fetched IDs
    WHEN remove_duplicate_logs is called
    THEN logs with duplicate UIDs are excluded from results.
    """
    logs = [{"uid": "1"}, {"uid": "2"}]
    last_run = {"Access Logs": {"fetched_events_ids": ["1"]}}
    result = remove_duplicate_logs(logs, last_run, ACCESS_LOGS)
    assert len(result) == 1
    assert result[0]["uid"] == "2"


def test_update_fetched_event_ids():
    """
    GIVEN a list of logs containing UIDs
    WHEN update_fetched_event_ids is called
    THEN the current_run dict stores fetched UIDs for deduplication.
    """
    current_run = {}
    logs = [
        {"uid": "A1", "event_date": "2025-11-13T05:30:59.000+00:00"},
        {"uid": "A2", "event_date": "2025-11-13T05:30:59" ".000+00:00"},
    ]
    update_fetched_event_ids(current_run, ACCESS_LOGS, logs, arg_to_datetime("2025-11-13T05:30:59.000+00:00"))
    assert current_run["Access Logs"]["fetched_events_ids"] == ["A1", "A2"]


# --- Compute Next Fetch Time ---


def test_compute_next_fetch_time_from_latest(access_event):
    """
    GIVEN a list of Access Log events
    WHEN compute_next_fetch_time is called
    THEN it returns the next fetch time slightly after the latest event timestamp.
    """
    events = [access_event]
    prev_time = datetime(2025, 11, 6, 4, 0, 0, tzinfo=UTC)
    ts = prev_time.timestamp()  # seconds (float)
    result = compute_next_fetch_time(events, int(ts * 1000), ACCESS_LOGS)
    assert "2025-11-06T04:24:12" in result


# --- Fetch Logic ---


def test_fetch_events_basic_flow(client):
    """
    GIVEN a mocked client returning one Access Log event
    WHEN fetch_events is executed
    THEN it returns updated current_run metadata and a list of fetched events.
    """
    first_fetch_time = datetime(2025, 11, 6, 0, 0, tzinfo=UTC)
    last_run = {}
    max_events = {ACCESS_LOGS: 10}

    current_run, events = fetch_events(client, last_run, first_fetch_time, [ACCESS_LOGS], max_events)
    assert isinstance(current_run, dict)
    assert "Access Logs" in current_run
    assert len(events) > 0
    assert "next_fetch_time" in current_run["Access Logs"]


def test_access_logs_fetch_with_same_second_overlap(monkeypatch):
    """
    GIVEN:
        - First fetch occurs with an event at t = 04:24:12.250
        - Next fetch is computed at t + 1ms = 04:24:12.251
        - Access Logs timestamps round down to seconds
        - Second fetch returns an event that happened at 04:24:12.750 (same second)
    WHEN:
        - fetch_events is called twice sequentially
    THEN:
        - The second event is fetched (not skipped)
        - Deduplication prevents re-fetch of the first event
    """

    from DecyfirEventCollector import (
        Client,
        fetch_events,
        ACCESS_LOGS,
        get_after_param,
    )
    from datetime import datetime, timedelta, UTC

    # --- Setup ---
    base_time = datetime(2025, 11, 6, 4, 24, 12, tzinfo=UTC)

    # Simulate a client returning one event at t=base_time
    first_event = {
        "uid": "event1",
        "event_date": base_time.isoformat(),
        "event_type": "AUTH",
    }
    second_event = {
        "uid": "event2",
        "event_date": base_time.isoformat(),  # same second!
        "event_type": "AUTH",
    }

    client = Client("https://test.com", verify=False, proxy=False, api_key="abc")

    # First _http_request returns event1
    monkeypatch.setattr(client, "_http_request", lambda **_: [first_event])

    first_fetch_time = base_time - timedelta(seconds=1)
    max_events = {ACCESS_LOGS: 10}

    # --- First fetch ---
    last_run = {}
    current_run, events = fetch_events(client, last_run, first_fetch_time, [ACCESS_LOGS], max_events)
    assert len(events) == 1
    assert events[0]["uid"] == "event1"

    # --- Second fetch ---
    # Now client returns event2 (same second but different UID)
    monkeypatch.setattr(client, "_http_request", lambda **_: [first_event, second_event])

    current_run2, events2 = fetch_events(client, current_run, first_fetch_time, [ACCESS_LOGS], max_events)

    # --- Assertions ---
    assert len(events2) == 1, "Should fetch the new event even if same second"
    assert events2[0]["uid"] == "event2"
    assert events2[0]["_time"].startswith("2025-11-06T04:24:12")
    # Ensure dedup did not remove this valid event
    assert current_run2[ACCESS_LOGS]["fetched_events_ids"] == ["event1", "event2"]

    # The 'after' timestamp used in the second fetch should be rounded down to nearest second
    after2 = get_after_param(current_run2, ACCESS_LOGS, first_fetch_time)
    assert after2 % 1000 == 0, "Access Logs timestamps must round to seconds"

    current_run3, events3 = fetch_events(client, current_run2, first_fetch_time, [ACCESS_LOGS], max_events)

    # --- Assertions ---
    assert len(events3) == 0, "Should fetch no events - all are duplicates"

    # Ensure dedup did not remove this valid event
    assert current_run3[ACCESS_LOGS]["fetched_events_ids"] == ["event1", "event2"]

    # The 'after' timestamp used in the third fetch should be rounded down to nearest second
    after3 = get_after_param(current_run3, ACCESS_LOGS, first_fetch_time)
    assert after3 % 1000 == 0, "Access Logs timestamps must round to seconds"


# --- Test Module ---


def test_test_module_returns_ok(client):
    """
    GIVEN a valid client and configuration
    WHEN test_module is called
    THEN it runs fetch_events successfully and returns 'ok'.
    """
    result = test_module(client)
    assert result == "ok"


# --- Manual get-events Command ---


def test_get_events_command_without_push(monkeypatch, client):
    """
    GIVEN should_push=False and mocked tableToMarkdown
    WHEN get_events_command is executed
    THEN human-readable tables are generated but no push occurs.
    """
    called = {}

    def fake_table(name, t):
        called["table"] = True
        return f"### {name}"

    monkeypatch.setattr("DecyfirEventCollector.tableToMarkdown", fake_table)
    get_events_command(client, [ACCESS_LOGS], {ACCESS_LOGS: 10}, None, should_push=False)
    assert "table" in called


def test_get_events_command_with_push(monkeypatch, client):
    """
    GIVEN should_push=True
    WHEN get_events_command is executed
    THEN send_events_to_xsiam is called with correct vendor and product.
    """
    called = {}

    def fake_send(events, vendor, product):
        called["sent"] = len(events)
        assert vendor == "cyfirma"
        assert product == "decyfir"

    monkeypatch.setattr("DecyfirEventCollector.send_events_to_xsiam", fake_send)
    get_events_command(client, [ACCESS_LOGS], {ACCESS_LOGS: 10}, None, should_push=True)
    assert called["sent"] > 0