DeCYFIR Feed

DeCYFIR API's provides External Threat Landscape Management insights.

Data Enrichment & Threat Intelligence · DeCYFIR · Feed

Details

IDDeCYFIR Feed
ProviderCYFIRMA
CategoryData Enrichment & Threat Intelligence
From Version6.9.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 v1 of DeCYFIR Feed

Configure DeCYFIR Indicators & Threat Intelligence Feed in Cortex

Parameter Description Required
DeCYFIR Server URL (e.g. https://decyfir.cyfirma.com)   True
DeCYFIR API Key   True
Trust any certificate (not secure)   False
Use system proxy settings   False
Fetch indicators   False
Indicator Reputation Indicators from this integration instance will be marked with this reputation. False
Source Reliability Reliability of the source providing the intelligence data. True
    False
    False
Feed Fetch Interval   False
Bypass exclusion list When selected, the exclusion list is ignored for indicators from this feed. This means that if an indicator from this feed is on the exclusion list, the indicator might still be added to the system. False
Tags Supports CSV values. False
Traffic Light Protocol Color The Traffic Light Protocol (TLP) designation to apply to indicators fetched from the feed 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-indicators


Gets indicators from the feed.

Base Command

decyfir-get-indicators

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

decyfir-ip-get


Get IP indicators from CYFIRMA.

Base Command

decyfir-ip-get

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command example

!decyfir-ip-get

Human Readable Output

IP indicators retrieved successfully.

decyfir-domain-get


Get Domain indicators from CYFIRMA.

Base Command

decyfir-domain-get

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command example

!decyfir-domain-get

Human Readable Output

Domain indicators retrieved successfully.

decyfir-url-get


Get URL indicators from CYFIRMA.

Base Command

decyfir-url-get

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command example

!decyfir-url-get

Human Readable Output

URL indicators retrieved successfully.

decyfir-file-get


Get File Hash indicators from CYFIRMA.

Base Command

decyfir-file-get

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command example

!decyfir-file-get

Human Readable Output

File indicators retrieved successfully.

Configuration parameters

  • url — DeCYFIR Server URL (e.g. https://decyfir.cyfirma.com) (required)
  • api_key — (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • feed — Fetch indicators
  • feedReputation — Indicator Reputation
  • feedReliability — Source Reliability (required)
  • feedExpirationPolicy
  • feedExpirationInterval
  • feedFetchInterval — Feed Fetch Interval
  • feedBypassExclusionList — Bypass exclusion list
  • feedTags — Tags
  • tlp_color — Traffic Light Protocol Color

Commands (5)

  • decyfir-domain-get

    Get Domain indicators from CYFIRMA.

  • decyfir-file-get

    Get File Hash indicators from CYFIRMA.

  • decyfir-get-indicators Deprecated

    Deprecated. Use the decyfir-ip-get, decyfir-domain-get, decyfir-url-get, or decyfir-file-get commands instead.

  • decyfir-ip-get

    Get IP indicators from CYFIRMA.

  • decyfir-url-get

    Get URL indicators from CYFIRMA.

import json
from unittest.mock import MagicMock


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


def _client():
    from decyfiriocs import Client

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


def _minimal_ta(confidence=85, name="TestActor", ta_type="threat-actor"):
    return {
        "type": ta_type,
        "id": f"{ta_type}--001",
        "name": name,
        "confidence": confidence,
        "description": "desc",
        "created": "2024-01-01T00:00:00Z",
        "modified": "2024-06-01T00:00:00Z",
        "extensions": {
            "extension-definition--abc": {
                "origin-of-country": "RU",
                "target-countries": ["US"],
                "target-industries": ["Finance"],
                "technologies": ["Windows"],
            }
        },
    }


def _minimal_cve(confidence=85):
    return {
        "type": "vulnerability",
        "id": "vulnerability--001",
        "name": "CVE-2024-99999",
        "confidence": confidence,
        "description": "test cve",
        "created": "2024-01-01T00:00:00Z",
        "modified": "2024-06-01T00:00:00Z",
        "extensions": {
            "extension-definition--abc": {
                "cvss_score": "9.8",
                "cvss_vector": "AV:N/AC:L/Au:N/C:C/I:C/A:C",
                "cvss_version": "2.0",
                "cvss_metrics_data": {
                    "availability_impact": "COMPLETE",
                    "integrity_impact": "COMPLETE",
                    "impact_score": 10.0,
                    "exploitability_score": 10.0,
                    "confidentiality_impact": "COMPLETE",
                    "access_vector": "NETWORK",
                    "access_complexity": "LOW",
                    "authentication": "NONE",
                    "vendors": ["VendorA", "VendorB"],
                    "products": ["ProdA"],
                    "technologies": ["Linux"],
                },
            }
        },
    }


def test_command_results_empty():
    from decyfiriocs import command_results

    result = command_results([], "IP", "")
    assert result.readable_output == "For current request no indicators found."


def test_extract_ioc_value():
    from decyfiriocs import extract_value

    result = extract_value("[ipv4-addr:value = '1.2.3.4']")
    assert result == "1.2.3.4"


def test_extract_value_empty_string():
    from decyfiriocs import extract_value

    result = extract_value("")
    assert result == ""


def test_get_indicator_or_threatintel_type(mocker):
    client = _client()
    da = client.get_indicator_or_threatintel_type("[ipv4-addr:value = '0.0.0.0']")
    assert da == "IP"


def test_get_indicator_or_threatintel_type_none():
    client = _client()
    result = client.get_indicator_or_threatintel_type(None)
    assert result == ""


def test_get_indicator_or_threatintel_type_unknown():
    client = _client()
    result = client.get_indicator_or_threatintel_type("something-unrecognized")
    assert result == ""


def test_get_indicator_or_threatintel_type_email():
    client = _client()
    result = client.get_indicator_or_threatintel_type("[email:value = 'foo@bar.com']")
    assert result == "Email"


def test_get_indicator_or_threatintel_type_vulnerability():
    client = _client()
    result = client.get_indicator_or_threatintel_type("vulnerability")
    assert result == "CVE"


def test_get_indicator_or_threatintel_type_threat_actor():
    client = _client()
    result = client.get_indicator_or_threatintel_type("threat-actor")
    assert result == "Threat Actor"


def test_get_indicator_or_threatintel_type_campaign():
    client = _client()
    result = client.get_indicator_or_threatintel_type("campaign")
    assert result == "Campaign"


def test_get_indicator_or_threatintel_type_malware():
    client = _client()
    result = client.get_indicator_or_threatintel_type("malware")
    assert result == "Malware"


def test_get_indicator_or_threatintel_type_attack_pattern():
    client = _client()
    result = client.get_indicator_or_threatintel_type("attack-pattern")
    assert result == "Attack Pattern"


def test_get_indicator_or_threatintel_type_intrusion_set():
    client = _client()
    result = client.get_indicator_or_threatintel_type("intrusion-set")
    assert result == "Intrusion Set"


def test_module_command_ok(mocker):
    from decyfiriocs import test_module_command

    client = _client()
    mock_resp = MagicMock()
    mock_resp.status_code = 200
    mocker.patch.object(client, "_http_request", return_value=mock_resp)

    result = test_module_command(client, "test_api_key")
    assert result == "ok"


def test_module_command_unauthorized(mocker):
    from decyfiriocs import test_module_command

    client = _client()
    mock_resp = MagicMock()
    mock_resp.status_code = 401
    mocker.patch.object(client, "_http_request", return_value=mock_resp)
    result = test_module_command(client, "test_api_key")
    assert result == "Not Authorized"


def test_module_command_forbidden(mocker):
    from decyfiriocs import test_module_command

    client = _client()
    mock_resp = MagicMock()
    mock_resp.status_code = 403
    mocker.patch.object(client, "_http_request", return_value=mock_resp)
    result = test_module_command(client, "test_api_key")
    assert result == "Not Authorized"


def test_fetch_indicators_by_type_error(mocker):
    client = _client()
    mocker.patch("demistomock.error")
    mocker.patch.object(client, "_http_request", side_effect=Exception("network error"))
    result = client.fetch_indicators_by_type("api_key", "ip")
    assert result == []


def test_build_ioc_relationship_obj(mocker):
    raw_iocs_ti_data = util_load_json("test_data/iocs.json")
    ta_data = raw_iocs_ti_data["IN_DATA_3"]
    iocs_data = raw_iocs_ti_data["IN_DATA_1"]

    client = _client()
    da = client.build_ioc_relationship_obj(iocs_data[0], ta_data[0])
    assert da is not None
    assert raw_iocs_ti_data["IN_DATA_5"][0] == da
    assert da["entityA"] == "0.0.0.0"
    assert da["entityB"] == "Gibberish Panda"
    assert da["name"] == "indicator-of"


def test_build_ioc_relationship_obj_fail(mocker):
    client = _client()
    da = client.build_ioc_relationship_obj({}, {})
    assert da is None


def test_build_ioc_relationship_obj_cve_ok(mocker):
    raw_data = util_load_json("test_data/iocs_ti.json")
    raw_cve_data = raw_data["cve"]

    client = _client()
    da_re = client.build_threat_intel_indicator_obj(data=raw_cve_data[0], tlp_color="tlp_color", feed_tags=["feedTags"])
    assert da_re["type"] == "CVE"
    assert da_re["name"] == "CVE-2025-61882"
    assert ("rawJSON" in da_re) is True


def test_build_ioc_relationship_obj_malware(mocker):
    raw_data = util_load_json("test_data/iocs_ti.json")
    raw_malware_data = raw_data["malware"]

    client = _client()
    da_re = client.build_threat_intel_indicator_obj(data=raw_malware_data[0], tlp_color="tlp_color", feed_tags=["feedTags"])
    assert da_re["value"] == "Lokibot "
    assert da_re["type"] == "Malware"
    assert ("rawJSON" in da_re) is True


def test_build_threat_intel_indicator_obj(mocker):
    raw_ti_data = util_load_json("test_data/iocs_ti.json")

    client = _client()

    da_re = client.build_threat_intel_indicator_obj(
        data=raw_ti_data["threat_actors"][0], tlp_color="tlp_color", feed_tags=["feedTags"]
    )
    assert da_re["value"] == "Gamaredon"
    assert da_re["type"] == "Threat Actor"
    assert ("rawJSON" in da_re) is True


def test_build_threat_intel_indicator_obj_fail(mocker):
    raw_ti_data = util_load_json("test_data/iocs_ti.json")

    client = _client()

    da_re = client.build_threat_intel_indicator_obj(data=raw_ti_data["iocs"][0], tlp_color="tlp_color", feed_tags=["feedTags"])
    assert da_re == {}


def test_build_ta_relationships_data(mocker):
    raw_iocs_ti_data = util_load_json("test_data/iocs_ti.json")
    raw_ti_data = raw_iocs_ti_data["ta_relationships"]

    client = _client()
    ta_source_obj = {}
    return_data = []

    ta_source_obj, src_ti_relationships_data, return_data = client.build_ta_relationships_data(
        ta_rel_data_coll=raw_ti_data,
        ta_source_obj=ta_source_obj,
        return_data=return_data,
        tlp_color="tlp_color",
        feed_tags=["feedTags"],
    )
    assert ta_source_obj["name"] == "Gamaredon"
    assert ("rawJSON" in ta_source_obj) is True


def test_build_threat_actor_relationship_obj(mocker):
    raw_iocs_ti_data = util_load_json("test_data/iocs_ti.json")
    raw_ti_data = raw_iocs_ti_data["ta_relationships"]

    client = _client()

    source_data = client.build_threat_intel_indicator_obj(data=raw_ti_data[0], tlp_color="tlp_color", feed_tags=["feedTags"])
    target_data = client.build_threat_intel_indicator_obj(data=raw_ti_data[1], tlp_color="tlp_color", feed_tags=["feedTags"])
    da_re = client.build_threat_actor_relationship_obj(source_data=source_data, target_data=target_data)

    assert da_re is not None
    assert da_re["entityA"] == "Gamaredon"
    assert da_re["entityAType"] == "Threat Actor"
    assert da_re["entityB"] == "CVE-2021-40444"
    assert da_re["entityBType"] == "CVE"
    assert da_re["name"] == "targets"


def test_convert_decyfir_ioc_to_indicators_formats(mocker):
    raw_iocs_ti_data = util_load_json("test_data/iocs_ti.json")
    ti_data = raw_iocs_ti_data["iocs"]

    client = _client()
    ti_data_out = client.convert_decyfir_ioc_to_indicators_formats(
        decyfir_api_key="api_key",
        decyfir_iocs=ti_data,
        tlp_color="tlp_color",
        feed_tags=["feedTags"],
        reputation="feedReputation",
        is_data_save=False,
    )

    assert ("rawJSON" in ti_data_out[0]) is True
    assert ti_data_out[0]["value"] == "0.0.0.0"
    assert ti_data_out[0]["type"] == "IP"
    assert ti_data_out[0]["fields"] is not None
    fields = ti_data_out[0]["fields"]
    assert fields["verdict"] == "Malicious"


def test_convert_decyfir_ioc_to_indicators_formats_different_value(mocker):
    raw_iocs_ti_data = util_load_json("test_data/iocs_ti.json")

    ti_data = raw_iocs_ti_data["iocs"]

    client = _client()
    ti_data_out = client.convert_decyfir_ioc_to_indicators_formats(
        decyfir_api_key="api_key",
        decyfir_iocs=ti_data,
        tlp_color="tlp_color",
        feed_tags=["feedTags"],
        reputation="feedReputation",
        is_data_save=False,
    )

    assert ("rawJSON" in ti_data_out[0]) is True
    assert ti_data_out[0]["value"] != "acme.com"
    assert ti_data_out[0]["type"] != "Domain"


def _make_ta_obj(confidence, name="Test Actor"):
    return {
        "type": "threat-actor",
        "id": "threat-actor--001",
        "name": name,
        "confidence": confidence,
        "description": "desc",
        "created": "2024-01-01T00:00:00Z",
        "modified": "2024-06-01T00:00:00Z",
        "extensions": {
            "extension-definition--abc": {
                "origin-of-country": "RU",
                "target-countries": ["US"],
                "target-industries": ["Finance"],
            }
        },
    }


def test_build_threat_intel_indicator_obj_suspicious():
    client = _client()
    result = client.build_threat_intel_indicator_obj(data=_make_ta_obj(60), tlp_color="GREEN", feed_tags=[])
    assert result["value"] == "Test Actor"
    assert result["fields"]["confidence"] == 60


def test_build_threat_intel_indicator_obj_benign():
    client = _client()
    result = client.build_threat_intel_indicator_obj(data=_make_ta_obj(20), tlp_color="GREEN", feed_tags=[])
    assert result["value"] == "Test Actor"
    assert result["fields"]["confidence"] == 20


def test_build_threat_intel_indicator_obj_no_extensions():
    obj = {
        "type": "threat-actor",
        "id": "threat-actor--002",
        "name": "Ghost",
        "confidence": 90,
        "description": "",
        "created": "2024-01-01T00:00:00Z",
        "modified": "2024-06-01T00:00:00Z",
        "extensions": {},
    }
    client = _client()

    result = client.build_threat_intel_indicator_obj(data=obj, tlp_color=None, feed_tags=[])
    assert result != {}


def test_build_threat_intel_indicator_obj_with_aliases_and_labels():
    obj = _make_ta_obj(85)
    obj["aliases"] = ["AKA1", "AKA2"]
    obj["labels"] = ["apt", "espionage"]
    client = _client()
    result = client.build_threat_intel_indicator_obj(data=obj, tlp_color="RED", feed_tags=["tag1"])

    assert "apt" in result["fields"]["tags"]


def test_fetch_indicators(mocker):
    from decyfiriocs import Client, fetch_indicators_command

    raw_data = util_load_json("test_data/iocs_ti.json")

    client = Client(
        base_url="test_url",
        verify=False,
    )
    mocker.patch.object(Client, "get_decyfir_api_ti_data", return_value=raw_data["iocs"])

    data = fetch_indicators_command(
        client=client, decyfir_api_key="api_key", tlp_color="tlp_color", reputation="feedReputation", feed_tags=["feedTags"]
    )
    assert ("rawJSON" in data[0]) is True
    assert data[0]["value"] == "0.0.0.0"
    assert data[0]["type"] == "IP"


def test_get_indicator_type(mocker):
    from decyfiriocs import Client

    client = Client(
        base_url="test_url",
        verify=False,
    )
    da = client.get_indicator_or_threatintel_type("[ipv4-addr:value = '0.0.0.0']")
    assert da == "IP"
    da = client.get_indicator_or_threatintel_type("[domain-name:value = 'mel8xo.cfd']")
    assert da == "Domain"
    da = client.get_indicator_or_threatintel_type("[url:value = 'http://acme[.]xyz/']")
    assert da == "URL"
    da = client.get_indicator_or_threatintel_type("[file:hashes.'SHA-256' = 'dbe51eabebf9d4ef9581ef99844a2944']")
    assert da == "File"


def test_decyfir_url_indicator_command(mocker):
    from decyfiriocs import Client, decyfir_url_indicator_command

    raw_data = util_load_json("test_data/iocs_ti.json")

    client = Client(
        base_url="test_url",
        verify=False,
    )
    mocker.patch.object(Client, "fetch_indicators_by_type", return_value=raw_data["url"])
    data = decyfir_url_indicator_command(client=client, decyfir_api_key="api_key")

    assert "http://acme[.]xyz/" in data.readable_output


def test_decyfir_domain_indicator_command(mocker):
    from decyfiriocs import Client, decyfir_domain_indicator_command

    raw_data = util_load_json("test_data/iocs_ti.json")

    client = Client(
        base_url="test_url",
        verify=False,
    )
    mocker.patch.object(Client, "fetch_indicators_by_type", return_value=raw_data["domain"])
    data = decyfir_domain_indicator_command(client=client, decyfir_api_key="api_key")

    assert "mel8xo.cfd" in data.readable_output


def test_decyfir_file_indicator_command(mocker):
    from decyfiriocs import Client, decyfir_hash_indicator_command

    raw_data = util_load_json("test_data/iocs_ti.json")

    client = Client(
        base_url="test_url",
        verify=False,
    )
    mocker.patch.object(Client, "fetch_indicators_by_type", return_value=raw_data["file"])
    data = decyfir_hash_indicator_command(client=client, decyfir_api_key="api_key")

    assert "dbe51eabebf9d4ef9581ef99844a2944" in data.readable_output


def test_decyfir_ip_indicator_command(mocker):
    from decyfiriocs import Client, decyfir_ip_indicator_command

    raw_data = util_load_json("test_data/iocs_ti.json")

    client = Client(
        base_url="test_url",
        verify=False,
    )
    mocker.patch.object(Client, "fetch_indicators_by_type", return_value=raw_data["iocs"])
    data = decyfir_ip_indicator_command(client=client, decyfir_api_key="api_key")

    assert "0.0.0.0" in data.readable_output


def test_decyfir_get_indicators_command(mocker):
    from decyfiriocs import Client, decyfir_get_indicators_command

    raw_data = util_load_json("test_data/iocs_ti.json")

    client = Client(
        base_url="test_url",
        verify=False,
    )
    mocker.patch.object(Client, "fetch_indicators", return_value=raw_data["iocs"])
    data = decyfir_get_indicators_command(
        client=client, decyfir_api_key="api_key", reputation="feedReputation", tlp_color="tlp_color", feed_tags=["feedTags"]
    )

    assert "0.0.0.0" in data.readable_output


def test_command_results(mocker):
    from decyfiriocs import command_results

    raw_data = util_load_json("test_data/iocs_ti.json")
    data = command_results(raw_data["file"], "File", "file indicators ")
    assert "dbe51eabebf9d4ef9581ef99844a2944" in data.readable_output


def test_get_decyfir_api_ti_data_non_200(mocker):
    client = _client()
    mock_resp = MagicMock()
    mock_resp.status_code = 403
    mock_resp.content = b""
    mocker.patch.object(client, "_http_request", return_value=mock_resp)
    result = client.get_decyfir_api_ti_data("/some/path")
    assert result == []


def test_get_decyfir_api_ti_data_200_empty_body(mocker):
    client = _client()
    mock_resp = MagicMock()
    mock_resp.status_code = 200
    mock_resp.content = b""
    mocker.patch.object(client, "_http_request", return_value=mock_resp)
    result = client.get_decyfir_api_ti_data("/some/path")
    assert result == []


def test_convert_ioc_with_is_data_save_true_cached_ta(mocker):
    raw_data = util_load_json("test_data/iocs_ti.json")
    ioc = raw_data["iocs"][0].copy()

    ioc["extensions"] = {
        list(ioc["extensions"].keys())[0]: {
            **list(ioc["extensions"].values())[0],
            "threat_actors": "Gamaredon",
        }
    }

    ta_bundle = raw_data["ta_relationships"]

    client = _client()
    mocker.patch.object(client, "get_decyfir_api_ti_data", return_value=ta_bundle)

    result = client.convert_decyfir_ioc_to_indicators_formats(
        decyfir_api_key="api_key",
        decyfir_iocs=[ioc, ioc],
        tlp_color="GREEN",
        feed_tags=["tag1"],
        reputation="Good",
        is_data_save=True,
    )

    assert any(r.get("value") == "0.0.0.0" for r in result)


def test_convert_ioc_with_is_data_save_true_no_ta(mocker):
    raw_data = util_load_json("test_data/iocs_ti.json")
    ioc = raw_data["iocs"][0].copy()
    ext_key = list(ioc["extensions"].keys())[0]
    ioc["extensions"] = {ext_key: {**list(ioc["extensions"].values())[0], "threat_actors": ""}}

    client = _client()
    mock_get = mocker.patch.object(client, "get_decyfir_api_ti_data", return_value=[])

    result = client.convert_decyfir_ioc_to_indicators_formats(
        decyfir_api_key="api_key",
        decyfir_iocs=[ioc],
        tlp_color="GREEN",
        feed_tags=[],
        reputation="Good",
        is_data_save=True,
    )

    mock_get.assert_not_called()
    assert len(result) == 1


def test_convert_ioc_with_is_data_save_true_unknown_ta(mocker):
    raw_data = util_load_json("test_data/iocs_ti.json")
    ioc = raw_data["iocs"][0].copy()
    ext_key = list(ioc["extensions"].keys())[0]
    ioc["extensions"] = {ext_key: {**list(ioc["extensions"].values())[0], "threat_actors": "Unknown"}}

    client = _client()
    mock_get = mocker.patch.object(client, "get_decyfir_api_ti_data", return_value=[])

    result = client.convert_decyfir_ioc_to_indicators_formats(
        decyfir_api_key="api_key",
        decyfir_iocs=[ioc],
        tlp_color=None,
        feed_tags=None,
        reputation=None,
        is_data_save=True,
    )

    mock_get.assert_not_called()
    assert len(result) == 1


def test_fetch_indicators_is_data_save_true(mocker):
    from decyfiriocs import fetch_indicators_command

    raw_data = util_load_json("test_data/iocs_ti.json")
    client = _client()
    mocker.patch.object(client, "get_decyfir_api_ti_data", return_value=raw_data["iocs"])

    result = fetch_indicators_command(
        client=client,
        decyfir_api_key="api_key",
        tlp_color="GREEN",
        reputation="Good",
        feed_tags=["tag1"],
    )

    assert len(result) > 0
    assert result[0]["value"] == "0.0.0.0"


def test_fetch_indicators_exception(mocker):
    client = _client()
    mocker.patch("demistomock.error")
    mocker.patch.object(client, "get_decyfir_api_ti_data", side_effect=Exception("API error"))

    result = client.fetch_indicators(
        decyfir_api_key="api_key",
        reputation="Good",
        tlp_color=None,
        feed_tags=[],
        is_data_save=True,
    )

    assert result == []


def test_build_ioc_relationship_obj_none_ioc():
    client = _client()
    result = client.build_ioc_relationship_obj({}, {"type": "Threat Actor", "value": "X"})
    assert result is None


def test_build_ioc_relationship_obj_none_ta():
    client = _client()
    result = client.build_ioc_relationship_obj({"type": "IP", "value": "1.2.3.4"}, {})
    assert result is None


def test_build_threat_actor_relationship_obj_source_fallback():
    client = _client()
    source = {"type": "Intrusion Set", "value": "APT28"}
    target = {"type": "some-unknown-type", "value": "unknown_target"}
    result = client.build_threat_actor_relationship_obj(source, target)
    assert result is not None
    assert result["name"] == "attributed-to"


def test_build_threat_actor_relationship_obj_no_mapping():
    client = _client()
    source = {"type": "unknown-src", "value": "src_val"}
    target = {"type": "unknown-tgt", "value": "tgt_val"}
    result = client.build_threat_actor_relationship_obj(source, target)
    assert result is None


def _base_params():
    return {
        "url": "test_url",
        "api_key": {"password": "test_api_key"},
        "insecure": False,
        "proxy": False,
        "feedTags": [],
        "tlp_color": "GREEN",
        "feedReputation": "Good",
    }


def test_main_decyfir_get_indicators(mocker):
    from decyfiriocs import main

    client = _client()
    raw_data = util_load_json("test_data/iocs_ti.json")
    mocker.patch("demistomock.params", return_value=_base_params())
    mocker.patch("demistomock.command", return_value="decyfir-get-indicators")
    mocker.patch("demistomock.info")
    mocker.patch("demistomock.error")
    mocker.patch.object(client, "fetch_indicators", return_value=raw_data["iocs"])
    mock_return = mocker.patch("decyfiriocs.return_results")

    main()
    assert mock_return.called


def test_main_ip_command(mocker):
    from decyfiriocs import main

    client = _client()

    raw_data = util_load_json("test_data/iocs_ti.json")
    mocker.patch("demistomock.params", return_value=_base_params())
    mocker.patch("demistomock.command", return_value="decyfir-ip-get")
    mocker.patch("demistomock.info")
    mocker.patch("demistomock.error")
    mocker.patch.object(client, "fetch_indicators_by_type", return_value=raw_data["iocs"])
    mock_return = mocker.patch("decyfiriocs.return_results")

    main()
    assert mock_return.called


def test_main_domain_command(mocker):
    from decyfiriocs import main

    client = _client()

    raw_data = util_load_json("test_data/iocs_ti.json")
    mocker.patch("demistomock.params", return_value=_base_params())
    mocker.patch("demistomock.command", return_value="decyfir-domain-get")
    mocker.patch("demistomock.info")
    mocker.patch("demistomock.error")
    mocker.patch.object(client, "fetch_indicators_by_type", return_value=raw_data["domain"])
    mock_return = mocker.patch("decyfiriocs.return_results")

    main()
    assert mock_return.called


def test_main_url_command(mocker):
    from decyfiriocs import main

    client = _client()
    raw_data = util_load_json("test_data/iocs_ti.json")
    mocker.patch("demistomock.params", return_value=_base_params())
    mocker.patch("demistomock.command", return_value="decyfir-url-get")
    mocker.patch("demistomock.info")
    mocker.patch("demistomock.error")
    mocker.patch.object(client, "fetch_indicators_by_type", return_value=raw_data["url"])
    mock_return = mocker.patch("decyfiriocs.return_results")

    main()
    assert mock_return.called


def test_main_file_command(mocker):
    from decyfiriocs import main

    client = _client()
    raw_data = util_load_json("test_data/iocs_ti.json")
    mocker.patch("demistomock.params", return_value=_base_params())
    mocker.patch("demistomock.command", return_value="decyfir-file-get")
    mocker.patch("demistomock.info")
    mocker.patch("demistomock.error")
    mocker.patch.object(client, "fetch_indicators_by_type", return_value=raw_data["file"])
    mock_return = mocker.patch("decyfiriocs.return_results")

    main()
    assert mock_return.called


def test_main_unknown_command(mocker):
    from decyfiriocs import main

    mocker.patch("demistomock.params", return_value=_base_params())
    mocker.patch("demistomock.command", return_value="not-a-real-command")
    mocker.patch("demistomock.info")
    mocker.patch("demistomock.error")
    mock_error = mocker.patch("decyfiriocs.return_error")

    main()
    assert mock_error.called


def test_main_exception_in_setup(mocker):
    from decyfiriocs import main

    mocker.patch("demistomock.params", side_effect=Exception("bad params"))
    mocker.patch("demistomock.command", return_value="test-module")
    mocker.patch("demistomock.error")
    mock_error = mocker.patch("decyfiriocs.return_error")

    main()
    assert mock_error.called


def test_convert_ioc_file_hash_sha256(mocker):
    client = _client()

    ioc = {
        "type": "indicator",
        "id": "indicator--sha256",
        "name": "File SHA-256 hash 'abc123'",
        "confidence": 90,
        "description": "",
        "created": "2024-01-01T00:00:00Z",
        "modified": "2024-06-01T00:00:00Z",
        "pattern": "[file:hashes.'SHA-256' = 'abc123']",
        "labels": [],
        "extensions": {
            "ext-1": {
                "threat_actors": "",
                "recommended_actions": "",
                "roles": "",
                "asn": "",
                "country_code": "",
            }
        },
    }
    result = client.convert_decyfir_ioc_to_indicators_formats(
        decyfir_api_key="key",
        decyfir_iocs=[ioc],
        reputation=None,
        tlp_color=None,
        feed_tags=None,
        is_data_save=False,
    )
    assert any(r.get("type") == "File" for r in result)


def test_convert_ioc_file_hash_sha1(mocker):
    client = _client()

    ioc = {
        "type": "indicator",
        "id": "indicator--sha1",
        "name": "File SHA-1 hash 'deadbeef'",
        "confidence": 55,
        "description": "",
        "created": "2024-01-01T00:00:00Z",
        "modified": "2024-06-01T00:00:00Z",
        "pattern": "[file:hashes.'SHA-1' = 'deadbeef']",
        "labels": [],
        "extensions": {
            "ext-1": {
                "threat_actors": "",
                "recommended_actions": "monitor",
                "roles": "",
                "asn": "",
                "country_code": "",
            }
        },
    }
    result = client.convert_decyfir_ioc_to_indicators_formats(
        decyfir_api_key="key",
        decyfir_iocs=[ioc],
        reputation=None,
        tlp_color=None,
        feed_tags=None,
        is_data_save=False,
    )
    assert any(r.get("type") == "File" for r in result)


def test_convert_ioc_file_hash_md5_fail(mocker):
    client = _client()

    ioc = {
        "type": "indicator",
        "id": "indicator--md5",
        "name": "File MD5 hash 'cafebabe'",
        "confidence": 30,
        "description": "",
        "created": "2024-01-01T00:00:00Z",
        "modified": "2024-06-01T00:00:00Z",
        "pattern": "[file:hashes.MD5 = 'cafebabe']",
        "labels": [],
        "extensions": {
            "ext-1": {
                "threat_actors": "",
                "recommended_actions": "",
                "roles": "",
                "asn": "",
                "country_code": "",
            }
        },
    }
    result = client.convert_decyfir_ioc_to_indicators_formats(
        decyfir_api_key="key",
        decyfir_iocs=[ioc],
        reputation=None,
        tlp_color=None,
        feed_tags=None,
        is_data_save=False,
    )
    assert any(r.get("type") != "File" for r in result)


def test_main_top_level_exception(mocker):
    from decyfiriocs import main

    mocker.patch("demistomock.params", side_effect=Exception("bad config"))
    mocker.patch("demistomock.command", return_value="test-module")
    mock_err = mocker.patch("decyfiriocs.return_error")
    main()
    assert mock_err.called


def _ioc_dict(threat_actors="", confidence=90):
    return {
        "type": "indicator",
        "id": "indicator--001",
        "name": "0.0.0.0",
        "confidence": confidence,
        "description": "test ioc",
        "created": "2024-01-01T00:00:00Z",
        "modified": "2024-06-01T00:00:00Z",
        "pattern": "[ipv4-addr:value = '0.0.0.0']",
        "labels": ["malicious"],
        "extensions": {
            "extension-definition--abc": {
                "threat_actors": threat_actors,
                "recommended_actions": "block",
                "roles": "botnet",
                "asn": "AS1234",
                "country_code": "RU",
            }
        },
    }


def test_fetch_indicators_is_data_save_false_slices(mocker):
    client = _client()

    iocs = [_ioc_dict() for _ in range(5)]
    for i, ioc in enumerate(iocs):
        ioc["id"] = f"indicator--{i:03}"
    mocker.patch.object(client, "get_decyfir_api_ti_data", return_value=iocs)

    result = client.fetch_indicators(
        decyfir_api_key="key",
        reputation=None,
        tlp_color=None,
        feed_tags=[],
        is_data_save=False,
    )
    assert len(result) == 2


def test_convert_ioc_is_data_save_true_empty_ta(mocker):
    client = _client()

    ioc = _ioc_dict(threat_actors="")
    mock_get = mocker.patch.object(client, "get_decyfir_api_ti_data", return_value=[])

    result = client.convert_decyfir_ioc_to_indicators_formats(
        decyfir_api_key="key",
        decyfir_iocs=[ioc],
        reputation="Good",
        tlp_color=None,
        feed_tags=None,
        is_data_save=True,
    )
    mock_get.assert_not_called()
    assert len(result) == 1


def test_build_ta_relationships_data_alternate_ref_keys():
    client = _client()
    ta = _minimal_ta(name="GhostActor")
    cve = _minimal_cve()
    rel = {
        "type": "relationship",
        "id": "relationship--001",
        "sourceRef": ta["id"],
        "targetRef": cve["id"],
    }
    bundle = [ta, cve, rel]

    ta_source_obj, rels, return_data = client.build_ta_relationships_data(
        ta_rel_data_coll=bundle,
        ta_source_obj={},
        return_data=[],
        tlp_color="GREEN",
        feed_tags=[],
    )
    assert ta_source_obj.get("name") == "GhostActor"