decyfir

DeCYFIR API's provides External Threat Landscape Management insights.

Data Enrichment & Threat Intelligence · DeCYFIR

Details

IDdecyfir
ProviderCYFIRMA
CategoryData Enrichment & Threat Intelligence
From Version6.5.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

DeCYFIR API’s provides External Threat Landscape Management insights.
This integration was integrated and tested with version v2 of decyfir

Configure DeCYFIR in Cortex

Parameter Description Required
Incident type   False
DeCYFIR Server URL (e.g. https://decyfir.cyfirma.com)   True
DeCYFIR API Key   True
Fetch incidents   False
Trust any certificate (not secure)   False
Use system proxy settings   False
How much time before the first fetch to retrieve incidents   False
Maximum number of incidents per fetch The maximum number of incidents to fetch per sub-category. 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-takedown-initiate


Initiate a take down request.

Base Command

decyfir-takedown-initiate

Input

Argument Name Description Required
alert_id The ID of the alert for which to initiate the take down request. Required

Context Output

There is no context output for this command.

Command example

!decyfir-takedown-initiate alert_id=123

Human Readable Output

The take down request was initiated successfully.

decyfir-takedown-list


Get take down list.

Base Command

decyfir-takedown-list

Input

Argument Name Description Required
sub_category The sub-category for which to retrieve the take down list. If not provided, the take down list for all sub-categories will be retrieved. Optional
size The number of records to retrieve. Default is 100. Optional
page The page number to retrieve. Default is 0. Optional

Context Output

There is no context output for this command.

Command example

!decyfir-takedown-list

Human Readable Output

The take down list retrieved successfully..

Configuration parameters

  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval
  • url — DeCYFIR Server URL (e.g. https://decyfir.cyfirma.com) (required)
  • api_key — (required)
  • isFetch — Fetch incidents
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • first_fetch — How much time before the first fetch to retrieve incidents
  • max_fetch — Maximum number of incidents per fetch

Commands (2)

  • decyfir-takedown-initiate

    Initiate a take down request.

  • decyfir-takedown-list

    Get take down list.

from datetime import datetime, timedelta
import json
from typing import cast, Any
from unittest.mock import MagicMock
import pytest


def util_load_json(path):
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


def _client():
    from decyfir import Client

    return Client(base_url="test_url", verify=False, proxy=False)


def test_fetch_incidents(mocker):
    from decyfir import Client, fetch_incidents

    date_format = "%Y-%m-%dT%H:%M:%SZ"
    raw_mock_response = util_load_json("test_data/raw_alerts.json")

    client = _client()
    mocker.patch.object(Client, "request_decyfir_api", return_value=raw_mock_response["alerts"])
    last_fetch = (datetime.now() - timedelta(days=80)).strftime(date_format)
    last_run = {"last_fetch": last_fetch}

    _, new_incidents = fetch_incidents(
        client=client,
        last_run=last_run,
        first_fetch="90 days",
        decyfir_api_key="api_key",
        incident_type="Attack Surface",
        max_fetch="1",
    )

    assert ("rawJSON" in new_incidents[0]) is True
    incident = cast(dict[str, Any], new_incidents[0])
    custom_fields: dict = dict(incident.get("customFields") or {})
    assert ("decyfirdatadetails" in custom_fields) is True


def test_get_take_down_list(mocker):
    from decyfir import Client, get_take_down_list

    raw_mock_response = util_load_json("test_data/raw_take_down_list.json")
    client = _client()
    mocker.patch.object(Client, "take_down_list_data", return_value=raw_mock_response)
    da = get_take_down_list(client=client, decyfir_api_key="api_key", args={"size": "1"})
    assert "T00001" in da.readable_output


def test_initiate_take_down_request(mocker):
    from decyfir import Client, initiate_take_down_request

    mock_response = util_load_json("test_data/raw_init_take_down.json")
    client = _client()

    mocker.patch.object(Client, "initiate_take_down", return_value=mock_response)
    da = initiate_take_down_request(client=client, decyfir_api_key="api_key", args={"uid": "63ac266713b0752aa7865100"})

    assert da.readable_output != ""


def test_get_severity_critical():
    from decyfir import IncidentSeverity

    client = _client()
    assert client.get_severity(9) == IncidentSeverity.CRITICAL


def test_get_severity_high():
    from decyfir import IncidentSeverity

    client = _client()
    assert client.get_severity(6) == IncidentSeverity.HIGH


def test_get_severity_medium():
    from decyfir import IncidentSeverity

    client = _client()
    assert client.get_severity(4) == IncidentSeverity.MEDIUM


def test_get_severity_low():
    from decyfir import IncidentSeverity

    client = _client()
    assert client.get_severity(1) == IncidentSeverity.LOW


def test_get_severity_unknown():
    from decyfir import IncidentSeverity

    client = _client()
    assert client.get_severity(0) == IncidentSeverity.UNKNOWN


def test_decyfir_api_request_200(mocker):
    client = _client()
    resp = MagicMock(status_code=200, content=b'[{"id":1}]')
    resp.json.return_value = [{"id": 1}]
    mocker.patch.object(client, "_http_request", return_value=resp)
    result = client.decyfir_api_request("/some/path")
    assert result == [{"id": 1}]


def test_decyfir_api_request_non_200(mocker):
    client = _client()
    resp = MagicMock(status_code=403, content=b"forbidden")
    mocker.patch.object(client, "_http_request", return_value=resp)
    assert client.decyfir_api_request("/some/path") == []


def test_decyfir_api_request_empty_content(mocker):
    client = _client()
    resp = MagicMock(status_code=200, content=b"")
    mocker.patch.object(client, "_http_request", return_value=resp)
    assert client.decyfir_api_request("/some/path") == []


def test_request_decyfir_api_200(mocker):
    client = _client()
    resp = MagicMock(status_code=200, content=b'[{"alert":"data"}]')
    resp.json.return_value = [{"alert": "data"}]
    mocker.patch.object(client, "_http_request", return_value=resp)
    result = client.request_decyfir_api("attack-surface", "open-ports", "&key=k")
    assert result == [{"alert": "data"}]


def test_request_decyfir_api_non_200(mocker):
    client = _client()
    resp = MagicMock(status_code=401, content=b"unauth")
    mocker.patch.object(client, "_http_request", return_value=resp)
    assert client.request_decyfir_api("attack-surface", "open-ports", "&key=k") == []


def test_request_decyfir_api_empty_content(mocker):
    client = _client()
    resp = MagicMock(status_code=200, content=b"")
    mocker.patch.object(client, "_http_request", return_value=resp)
    assert client.request_decyfir_api("attack-surface", "open-ports", "&key=k") == []


def test_initiate_take_down_200(mocker):
    client = _client()
    resp = MagicMock(status_code=200, content=b'{"response":{"ticketName":"T001"}}')
    resp.json.return_value = {"response": {"ticketName": "T001"}}
    mocker.patch.object(client, "_http_request", return_value=resp)
    result = client.initiate_take_down("api_key", "alert-001")
    assert result == {"response": {"ticketName": "T001"}}


def test_initiate_take_down_non_200(mocker):
    client = _client()
    resp = MagicMock(status_code=400, content=b"bad request")
    mocker.patch.object(client, "_http_request", return_value=resp)
    result = client.initiate_take_down("api_key", "alert-001")
    assert result == {}


def test_initiate_take_down_empty_content(mocker):
    client = _client()
    resp = MagicMock(status_code=200, content=b"")
    mocker.patch.object(client, "_http_request", return_value=resp)
    result = client.initiate_take_down("api_key", "alert-001")
    assert result == {}


def test_initiate_take_down_request_no_data(mocker):
    from decyfir import initiate_take_down_request

    client = _client()
    mocker.patch.object(client, "initiate_take_down", return_value={})
    result = initiate_take_down_request(client, "api_key", {"alert_id": "a1"})
    assert "no data" in result.readable_output.lower()


def test_initiate_take_down_request_error_flag(mocker):
    from decyfir import initiate_take_down_request

    client = _client()
    mocker.patch.object(client, "initiate_take_down", return_value={"error": True, "response": {}})
    result = initiate_take_down_request(client, "api_key", {"alert_id": "a1"})
    assert "Error" in result.readable_output


def test_initiate_take_down_request_empty_response_key(mocker):
    from decyfir import initiate_take_down_request

    client = _client()
    mocker.patch.object(client, "initiate_take_down", return_value={"error": False, "response": {}})
    result = initiate_take_down_request(client, "api_key", {"alert_id": "a1"})
    assert "Error" in result.readable_output


def test_initiate_take_down_request_success(mocker):
    from decyfir import initiate_take_down_request

    client = _client()
    mocker.patch.object(
        client,
        "initiate_take_down",
        return_value={"error": False, "response": {"ticketName": "T00001"}},
    )
    result = initiate_take_down_request(client, "api_key", {"alert_id": "a1"})
    assert "T00001" in result.readable_output
    assert "Successfully" in result.readable_output


def test_get_take_down_list_empty(mocker):
    from decyfir import get_take_down_list

    client = _client()
    mocker.patch.object(client, "take_down_list_data", return_value=[])
    result = get_take_down_list(client, "api_key", {})
    assert result.readable_output == "No data found for current request."


def test_fetch_incidents_no_last_run(mocker):
    from decyfir import Client, fetch_incidents

    client = _client()
    mocker.patch.object(Client, "request_decyfir_api", return_value=[])
    last_fetch, incidents = fetch_incidents(
        client=client,
        last_run=None,
        first_fetch="30 days",
        decyfir_api_key="api_key",
        incident_type="Attack Surface",
        max_fetch="10",
    )
    assert "last_fetch" in last_fetch
    assert incidents == []


def test_fetch_incidents_with_last_run(mocker):
    from decyfir import Client, fetch_incidents

    client = _client()
    mocker.patch.object(Client, "request_decyfir_api", return_value=[])
    last_run = {"last_fetch": "2024-01-01T00:00:00Z"}
    last_fetch, incidents = fetch_incidents(
        client=client,
        last_run=last_run,
        first_fetch="30 days",
        decyfir_api_key="api_key",
        incident_type="Attack Surface",
        max_fetch="5",
    )
    assert "last_fetch" in last_fetch
    assert incidents == []


def test_fetch_incidents_forbidden_exception(mocker):
    from decyfir import Client, fetch_incidents

    client = _client()
    mocker.patch.object(Client, "get_decyfir_data", side_effect=Exception("403 Forbidden access denied"))
    result = fetch_incidents(
        client=client,
        last_run={"last_fetch": "2024-01-01T00:00:00Z"},
        first_fetch="30 days",
        decyfir_api_key="api_key",
        incident_type="Attack Surface",
        max_fetch="10",
    )
    assert "Authorization Error" in result


def test_fetch_incidents_generic_exception(mocker):
    from decyfir import Client, fetch_incidents

    client = _client()
    mocker.patch.object(Client, "get_decyfir_data", side_effect=Exception("network timeout"))
    with pytest.raises(Exception, match="network timeout"):
        fetch_incidents(
            client=client,
            last_run={"last_fetch": "2024-01-01T00:00:00Z"},
            first_fetch="30 days",
            decyfir_api_key="api_key",
            incident_type="Attack Surface",
            max_fetch="10",
        )


def test_fetch_incidents_all_incident_types(mocker):
    from decyfir import Client, fetch_incidents

    client = _client()
    mock_req = mocker.patch.object(Client, "request_decyfir_api", return_value=[])
    fetch_incidents(
        client=client,
        last_run=None,
        first_fetch="7 days",
        decyfir_api_key="api_key",
        incident_type=None,
        max_fetch=None,
    )
    # 6 AS + 4 II + 3 DB + 5 SPE = 18 calls
    assert mock_req.call_count == 18


def test_take_down_list_data_none_sub_category(mocker):
    client = _client()
    mocker.patch.object(client, "decyfir_api_request", return_value=[])
    result = client.take_down_list_data("api_key", None, "0", "50")
    assert result == []


def test_get_decyfir_data_attack_surface(mocker):
    from decyfir import LABEL_ATTACK_SURFACE

    client = _client()
    mocker.patch.object(client, "request_decyfir_api", return_value=[])
    result = client.get_decyfir_data(0, "api_key", LABEL_ATTACK_SURFACE, "10")
    # All 6 attack surface sub-types should be keys
    assert "open-ports" in result
    assert "ip-vulnerability" in result
    assert "configuration" in result
    assert "cloud-weakness" in result
    assert "ip-reputation" in result
    assert "certificates" in result


def test_get_decyfir_data_impersonation(mocker):
    from decyfir import LABEL_DIGITAL_RISK_IM_IN

    client = _client()
    mocker.patch.object(client, "request_decyfir_api", return_value=[])
    result = client.get_decyfir_data(0, "api_key", LABEL_DIGITAL_RISK_IM_IN, "10")
    assert "domain-it-asset" in result
    assert "executive-people" in result
    assert "product-solution" in result
    assert "social-handlers" in result


def test_get_decyfir_data_data_breach(mocker):
    from decyfir import LABEL_DIGITAL_RISK_DB_WM

    client = _client()
    mocker.patch.object(client, "request_decyfir_api", return_value=[])
    result = client.get_decyfir_data(0, "api_key", LABEL_DIGITAL_RISK_DB_WM, "10")
    assert "phishing" in result
    assert "ransomware" in result
    assert "dark-web" in result


def test_get_decyfir_data_social_exposure(mocker):
    from decyfir import LABEL_DIGITAL_RISK_S_PE

    client = _client()
    mocker.patch.object(client, "request_decyfir_api", return_value=[])
    result = client.get_decyfir_data(0, "api_key", LABEL_DIGITAL_RISK_S_PE, "10")
    assert "source-code" in result
    assert "malicious-mobile-apps" in result
    assert "confidential-files" in result
    assert "dumps-pii-cii" in result
    assert "social-threat" in result


def test_get_decyfir_data_no_incident_type_fetches_all(mocker):
    client = _client()
    mock_req = mocker.patch.object(client, "request_decyfir_api", return_value=[])
    client.get_decyfir_data(0, "api_key", "", None)
    # 6 AS + 4 II + 3 DBWM + 5 SPE = 18 total calls
    assert mock_req.call_count == 18


def test_get_decyfir_data_uses_max_fetch(mocker):
    client = _client()
    from decyfir import LABEL_ATTACK_SURFACE

    mock_req = mocker.patch.object(client, "request_decyfir_api", return_value=[])
    client.get_decyfir_data(12345, "api_key", LABEL_ATTACK_SURFACE, "42")
    # Verify that the api_param_query contains size=42 and after=12345
    call_args = mock_req.call_args_list[0]
    query_str = call_args[0][2]  # third positional arg
    assert "size=42" in query_str
    assert "after=12345" in query_str