ETDXsoarConnector

Fetches Cisco Email Threat Defense (ETD) message events and creates incidents.

Analytics & SIEM · ETD XSOAR Connector

Details

IDETDXsoarConnector
ProviderCisco
CategoryAnalytics & SIEM
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10404775

README

Fetches Cisco Email Threat Defense (ETD) message events and creates incidents.
This integration was integrated and tested with of ETDXsoarConnector.

Configure Cisco ETD Connector in Cortex

Parameter Required
ETD Base URL True
api_key True
API Key True
Client ID True
client_secret True
Client Secret True
Use system proxy settings False
Trust any certificate (not secure) False
First Fetch Time False
Fetch incidents  
Incidents Fetch Interval  
Incident type False
Max fetch 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.

cisco-etd-move-message


Reclassifies and remediates an ETD message.

Base Command

cisco-etd-move-message

Input

Argument Name Description Required
message_id The ETD message ID. Required
verdict The new verdict. Required
folder The new folder action. Required

Context Output

Path Type Description
ETD.Message.ID String The message ID.
ETD.Message.Verdict String The updated verdict.
ETD.Message.Folder String The updated folder.

Command Example

!cisco-etd-move-message message_id="123456789abcdef" verdict="malicious" folder="quarantine"

Context Example

{
  "ETD": {
    "Message": {
      "ID": "123456789abcdef",
      "Verdict": "malicious",
      "Folder": "quarantine"
    }
  }
}

Configuration parameters

  • etd_base_url — ETD Base URL (required)
  • api_key — (required)
  • client_id — Client ID (required)
  • client_secret — (required)
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)
  • first_fetch — First Fetch Time
  • isFetch — Fetch incidents
  • incidentFetchInterval — Incidents Fetch Interval
  • incidentType — Incident type
  • max_fetch — Max fetch

Commands (1)

  • cisco-etd-move-message

    Reclassifies and remediates an ETD message.

from datetime import datetime, UTC
import requests
import pytest
from CommonServerPython import DemistoException

from ETDXsoarConnector import (
    ETDClient,
    get_credential,
    generate_intervals,
    get_event_time,
    get_event_id,
    fetch_incidents,
    cisco_etd_move_message_command,
)
from ETDXsoarConnector import test_module as integration_test_module


def test_get_links():
    response = {"data": {"message": ["https://example.com/log1", "https://example.com/log2"]}}
    client = ETDClient.__new__(ETDClient)
    links = client.get_links(response)
    assert links == [
        "https://example.com/log1",
        "https://example.com/log2",
    ]


def test_get_credential_password():
    cred = {"password": "secret"}
    assert get_credential(cred) == "secret"


def test_get_credential_nested():
    cred = {"credentials": {"password": "mypassword"}}
    assert get_credential(cred) == "mypassword"


def test_get_credential_string():
    assert get_credential("apikey") == "apikey"


def test_generate_intervals():
    start = datetime(2026, 1, 1, 0, 0, tzinfo=UTC)
    end = datetime(2026, 1, 1, 9, 0, tzinfo=UTC)
    intervals = generate_intervals(start, end)
    assert len(intervals) == 3
    assert intervals[0][0] == start
    assert intervals[0][1] == datetime(2026, 1, 1, 3, tzinfo=UTC)


def test_test_module(mocker):
    client = mocker.Mock()
    client.request_log_export.return_value = {"data": {"message": ["dummy"]}}
    client.get_links.return_value = ["dummy"]
    client.download_logs.return_value = []
    assert integration_test_module(client) == "ok"


def test_get_event_time():
    event = {"message": {"timestamp": "2026-01-01T10:00:00Z"}}
    assert get_event_time(event) == "2026-01-01T10:00:00Z"


def test_get_event_id():
    event = {"message": {"id": "123"}}
    id1 = get_event_id(event)
    id2 = get_event_id(event)
    assert id1 == id2


def test_cisco_etd_move_message_command(mocker):
    client = ETDClient.__new__(ETDClient)
    client._http_request = mocker.Mock(return_value={"status": "success"})
    result = cisco_etd_move_message_command(
        client,
        {
            "message_id": "123",
            "verdict": "phishing",
            "folder": "quarantine",
        },
    )
    client._http_request.assert_called_once()
    assert "ETD message updated" in result.readable_output


def test_fetch_incidents_no_logs(mocker):
    client = mocker.Mock()
    client.request_log_export.return_value = {"data": {"message": []}}
    client.get_links.return_value = []
    mocker.patch("ETDXsoarConnector.demisto.getLastRun", return_value={})
    mocker.patch("ETDXsoarConnector.demisto.setLastRun")
    mocker.patch("ETDXsoarConnector.demisto.incidents")
    incidents = fetch_incidents(client, {"max_fetch": 10})
    assert incidents == []


def test_fetch_incidents(mocker):
    client = mocker.Mock()
    client.request_log_export.return_value = {"data": {"message": ["dummy"]}}
    client.get_links.return_value = ["dummy"]
    client.download_logs.return_value = [
        {
            "message": {
                "id": "123",
                "timestamp": "2026-07-01T10:00:00Z",
                "fromAddresses": "alice@example.com",
                "verdict": {"verdict": "phishing"},
            }
        }
    ]
    mocker.patch("ETDXsoarConnector.demisto.getLastRun", return_value={})
    mocker.patch("ETDXsoarConnector.demisto.setLastRun")
    mocker.patch("ETDXsoarConnector.demisto.incidents")
    incidents = fetch_incidents(client, {"max_fetch": 10})
    assert len(incidents) == 1
    assert incidents[0]["CustomFields"]["etdxsoarconnectormessageid"] == "123"


class MockResponse:
    status_code = 200
    text = '{"logType":"message","message":{"id":"1"}}\n'

    def raise_for_status(self):
        pass


def test_download_logs(monkeypatch):
    monkeypatch.setattr(requests, "get", lambda *args, **kwargs: MockResponse())
    client = ETDClient.__new__(ETDClient)
    client.params = {"insecure": False, "proxy": False}
    events = client.download_logs(["https://example.com/log"])
    assert len(events) == 1
    assert events[0]["message"]["id"] == "1"


def test_fetch_incidents_duplicate_event(mocker):
    event = {
        "message": {
            "id": "123",
            "timestamp": "2026-07-01T10:00:00Z",
            "fromAddresses": "alice@example.com",
            "verdict": {"verdict": "phishing"},
        }
    }
    client = mocker.create_autospec(ETDClient)
    client.request_log_export.return_value = {"data": {"message": ["dummy"]}}
    client.get_links.return_value = ["dummy"]
    client.download_logs.return_value = [event, event]
    mocker.patch("ETDXsoarConnector.demisto.getLastRun", return_value={})
    mocker.patch("ETDXsoarConnector.demisto.setLastRun")
    mocker.patch("ETDXsoarConnector.demisto.incidents")
    incidents = fetch_incidents(client, {"max_fetch": 10})
    assert len(incidents) == 1


def test_get_access_token_no_token(mocker):
    client = ETDClient.__new__(ETDClient)
    client.params = {"api_key": "apikey", "client_secret": "secret", "client_id": "client"}
    client._http_request = mocker.Mock(return_value={})

    with pytest.raises(DemistoException):
        client.get_access_token()


def test_get_event_time_invalid():
    event = {"message": {"timestamp": "invalid"}}
    assert get_event_time(event) is None


def test_get_links_empty():
    client = ETDClient.__new__(ETDClient)
    assert client.get_links({}) == []


class MockResponseNonMessage:
    status_code = 200
    text = '{"logType":"audit"}\n'

    def raise_for_status(self):
        pass


def test_download_logs_skip_non_message(monkeypatch):
    monkeypatch.setattr(requests, "get", lambda *args, **kwargs: MockResponseNonMessage())
    client = ETDClient.__new__(ETDClient)
    client.params = {"proxy": False, "insecure": False}
    events = client.download_logs(["http://dummy"])
    assert events == []


def test_fetch_incidents_invalid_first_fetch(mocker):
    client = mocker.Mock()
    mocker.patch("ETDXsoarConnector.demisto.getLastRun", return_value={})
    mocker.patch("ETDXsoarConnector.arg_to_datetime", return_value=None)
    with pytest.raises(DemistoException):
        fetch_incidents(client, {"first_fetch": "bad-date"})