ManageEngine

ManageEngine Endpoint Central is a Unified Endpoint Management solution that helps in managing thousands of servers, desktops, laptops and mobile devices from a single console..

Analytics & SIEM · ManageEngine

Details

IDManageEngine
ProviderZoho Corporation
CategoryAnalytics & SIEM
From Version8.3.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesXSIAM

README

ManageEngine Endpoint Central is a Unified Endpoint Management solution that helps in managing thousands of servers, desktops, laptops and mobile devices from a single console. This integration serves as an Event Collector for Cortex XSIAM, enabling fetching AuditLogs from Endpoint Central.

Endpoint Central Cloud Domains

Endpoint Central cloud is hosted at multiple data centers, and therefore available on different domains. There are several domains for Endpoint Central Cloud APIs, so you can use the one that is applicable to you.

Data Centre Domain EndpointCentral Server URI
United States .com https://endpointcentral.manageengine.com
Europe .eu https://endpointcentral.manageengine.eu
India .in https://endpointcentral.manageengine.in
Australia .com.au https://endpointcentral.manageengine.com.au
China .cn https://endpointcentral.manageengine.cn
Japan .jp https://endpointcentral.manageengine.jp
Canada .ca https://endpointcentral.manageengine.ca

The APIs on this page are intended for organizations hosted on the .com domain. If your organization is on a different domain, replace “.com” with the appropriate domain for the API endpoints before using them.
Note: You can also find out which domain you’re accessing by checking the URL while logged in to Endpoint Central.

Setting Up the Instance

Step 1: Generate Client ID and Client Secret

  1. Register your application as a new client by accessing the developer console.
  2. Choose Self client as application type.
  3. After choosing the client type, provide the required details and click ‘Create’. On successful registration, you will be provided with a set of OAuth 2.0 credentials such as Client_ID and Client_Secret that will be only known to Zoho and your application. (Do not share this credentials anywhere).

Step 2: Authorization by generating the grant token

After generating Client_ID and Client_Secret, a grant code has to be generated.
Self Client Method - For Self Client type.

  • After registration, click the Self Client method available on the Applications list.
  • Enter a valid scope: DesktopCentralCloud.Admin.READ

Click Create to generate Code.

Testing the configuration

To test the configuration, run the !manage-engine-test command instead of using the Test button.

Configure ManageEngine in Cortex

Parameter Required
Server URL True
Client ID True
Client Secret True
Code True
Trust any certificate (not secure) False
Use system proxy settings False
Max number of audit events per fetch False
Fetch events 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.

manage-engine-test


Tests connectivity of the server.

Base Command

manage-engine-test

manage-engine-get-events


Manual command to fetch events and display them.

Base Command

manage-engine-get-events

Input

Argument Name Description Required
should_push_events If true, the command will create events. Otherwise, it will only display them. Used for debugging purposes. Required
limit Maximum number of results to return. Optional
start_date Date from which to get events, For example ‘2018-11-06T08:56:41.000Z’. Optional
end_date Date to which to get events , For example ‘2018-11-06T08:56:41.000Z’. Optional

Context Output

There is no context output for this command.

Configuration parameters

  • server_url — Server URL (required)
  • credentials — Client ID (required)
  • client_code — Code (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • max_audit_events — Max number of audit events per fetch
  • isFetchEvents — Fetch events

Commands (2)

  • manage-engine-get-events

    Gets events from ManageEngine.

  • manage-engine-test

    Tests connectivity of the server.

import demistomock as demisto
from CommonServerPython import *
import urllib3

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
VENDOR = "manage"
PRODUCT = "engine"
PAGE_LIMIT_DEFAULT = 5000
DEFAULT_MAX_FETCH = 25000

ENDPOINT_TO_ZOHO_ACCOUNTS = {
    "https://endpointcentral.manageengine.com": "https://accounts.zoho.com",
    "https://endpointcentral.manageengine.eu": "https://accounts.zoho.eu",
    "https://endpointcentral.manageengine.in": "https://accounts.zoho.in",
    "https://endpointcentral.manageengine.com.au": "https://accounts.zoho.com.au",
    "https://endpointcentral.manageengine.cn": "https://accounts.zoho.cn",
    "https://endpointcentral.manageengine.jp": "https://accounts.zoho.jp",
    "https://endpointcentral.manageengine.ca": "https://accounts.zohoone.ca",
}


class Client(BaseClient):
    """Client class to interact with the service API"""

    def __init__(
        self,
        base_url: str,
        client_id: str,
        client_secret: str,
        client_code: str,
        verify: bool = True,
        proxy: bool = False,
        **kwargs,
    ):
        """
        Args:
            base_url: Base URL of the EndpointCentral instance.
            client_id: Client ID.
            client_secret: Client secret.
            client_code: Authorization code.
            verify: Whether to verify SSL certificates.
            proxy: Whether to use system proxy.
        """
        super().__init__(base_url=base_url, verify=verify, proxy=proxy, **kwargs)
        self.client_id = client_id
        self.client_secret = client_secret
        self.client_code = client_code

    def get_access_token_request(self, data: dict) -> dict[str, str]:
        """Token http_request wrapper.

        Args:
            data (dict): {"client_id":,
                          "client_secret":
                          "grant_type":"refresh_token"/"authorization_code"
                          "refresh_token"/"code":}

        Returns:
            dict: The response
        """
        return self._http_request(
            method="POST",
            full_url=ENDPOINT_TO_ZOHO_ACCOUNTS[self._base_url] + "/oauth/v2/token",
            data=data,
            resp_type="json",
            headers={"Content-Type": "application/x-www-form-urlencoded"},
        )

    def get_access_token(self) -> str:
        """
        Obtain or refresh an access token, caching it in the integration context.
        First run will use the code and next run will use the refresh token for creating the access token.

        Raises:
            DemistoException: If neither an authorization code nor refresh token is available.
        """
        ctx = get_integration_context() or {}

        access_token = ctx.get("access_token")
        expire_date_str = ctx.get("expire_date")
        now = datetime.now()
        if access_token and expire_date_str:
            expire_date = datetime.fromisoformat(expire_date_str)
            if now < expire_date:
                demisto.debug(f"Using cached access token. Expires at {expire_date_str}")
                return access_token

        demisto.debug("No valid access token exists.")

        data = {
            "client_id": self.client_id,
            "client_secret": self.client_secret,
        }

        refresh_token = ctx.get("refresh_token")

        if refresh_token:
            demisto.debug("Refresh token found in context")
            data.update({"grant_type": "refresh_token", "refresh_token": refresh_token})

        elif self.client_code:
            demisto.debug("Refresh token not found in context.")
            data.update({"grant_type": "authorization_code", "code": self.client_code})
        else:
            raise DemistoException(message="Either grant code or refresh token must be provided.")

        demisto.debug("Asking for new tokens")

        response = self.get_access_token_request(data)

        if "error" in response:
            demisto.debug("Error creating token")
            raise DemistoException(response.get("error"))
        access_token = response["access_token"]
        new_ctx = {
            "refresh_token": response.get("refresh_token") or ctx.get("refresh_token"),
            "access_token": access_token,
            "expire_date": (now + timedelta(seconds=int(response.get("expires_in", 0)) - 60)).isoformat(),
        }

        demisto.debug(f"New access token acquired and stored. Expires at {new_ctx['expire_date']}")
        set_integration_context(new_ctx)
        return access_token

    def search_events(self, start_time: str, end_time: str, limit: int) -> List[Dict]:  # noqa: E501
        """
        Paginate through audit logs.
        Logs return in random order from the API.
        API include start_time and end_time.

        Args:
            start_time_ms: UNIX‐ms timestamp to start from.
            end_time_ms: UNIX‐ms timestamp to end at.
            limit: Maximum total events to return.

        Returns:
            List of audit‐log dicts (up to `limit`) sorted by eventTime.
        """
        events: List[Dict] = []
        page = 1
        access_token = self.get_access_token()
        headers = {
            "Authorization": f"Zoho-oauthtoken {access_token}",
            "Accept": "application/auditlogsdata.v1+json",
        }

        params = {"startTime": start_time, "endTime": end_time, "pageLimit": PAGE_LIMIT_DEFAULT}
        demisto.debug(f"Time interval: {start_time} to {end_time}.")

        while True:
            params["page"] = str(page)

            response = self._http_request(
                method="GET",
                url_suffix="/emsapi/server/auditLogs",
                headers=headers,
                params=params,
                ok_codes=(200, 204),
                resp_type="json",
            )

            events_page = response.get("messageResponse", [])
            status = response.get("status")
            demisto.debug(f"Successfully fetched {len(events_page)} events on page {page}")
            if status != "success":
                demisto.debug(f"API returned status='{status}', stopping pagination.")
                break

            if not events_page:
                demisto.debug("No more events")
                break

            events.extend(events_page)
            page += 1

            if len(events_page) < PAGE_LIMIT_DEFAULT:
                demisto.debug(f"Fetched {len(events_page)} events < PAGE_LIMIT ({PAGE_LIMIT_DEFAULT}) on the last page.")
                break

        events.sort(key=lambda e: int(e["eventTime"]))
        return events[:limit]


def test_module(client: Client) -> str:
    """
    Verifies credentials + connectivity by attempting to fetch a single audit log.
    """
    now_ts = int(time.time() * 1000)
    one_min_ago_ts = now_ts - (60 * 1000)

    try:
        _ = client.search_events(str(one_min_ago_ts), str(now_ts), 1)
        return "Connection is valid."
    except Exception as e:
        msg = str(e).lower()
        if "unauthorized" in msg or "forbidden" in msg:
            return "Authorization Error: check client_id, client_secret, and client_code"
        raise


def get_events(client: Client, args: dict) -> CommandResults:
    """
    manage-engine-get-events command.
    """
    should_push = argToBoolean(args.get("should_push_events", "false"))
    limit = arg_to_number(args.get("limit")) or 10
    start_date_str = args.get("start_date")
    end_date_str = args.get("end_date")

    now_ts = int(time.time() * 1000)

    start_date = date_to_timestamp(start_date_str, DATE_FORMAT) if start_date_str else (now_ts - 60 * 1000)
    end_date = date_to_timestamp(end_date_str, DATE_FORMAT) if end_date_str else now_ts

    events = client.search_events(str(start_date), str(end_date), limit)
    add_time_to_events(events)
    human_readable = tableToMarkdown(
        name="ManageEngine Audit Logs",
        t=events,
    )
    results = CommandResults(
        readable_output=human_readable,
        outputs_prefix="ManageEngine.Event",
        outputs=events,
    )

    if should_push:
        send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)
    return results


def fetch_events(
    client: Client,
    last_run: dict[str, str],
    max_events_per_fetch: int,
) -> tuple[Dict, List[Dict]]:
    """
    Polling logic for `fetch-events`.

    Args:
        client: client to use.
        last_run: XSOAR lastRun dict, with `last_time` in ms.
        max_events: Max events to pull this cycle.

    Returns:
        next_run: dict with updated `last_time`;
        events: list of new events.
    """

    now_ts = int(time.time() * 1000)
    last_time_ts = (last_run.get("last_time")) or str(now_ts - (60 * 1000))
    demisto.debug(f"Fetching from: {timestamp_to_datestring(last_time_ts)} to {timestamp_to_datestring(now_ts)}")

    events = client.search_events(start_time=last_time_ts, end_time=str(now_ts), limit=max_events_per_fetch)

    add_time_to_events(events)
    demisto.debug(f"Fetched {len(events)} events.")
    max_timestamp = int(events[-1]["eventTime"] if events else now_ts) + 1
    last_run = {"last_time": f"{max_timestamp}"}
    return last_run, events


def add_time_to_events(events: List[Dict] | None):
    """
    Adds the _time key to the events.
    Args:
        events: List[Dict] - list of events to add the _time key to.
    Returns:
        list: The events with the _time key.
    """
    if events:
        for event in events:
            event["_time"] = timestamp_to_datestring(event.get("eventTime"), date_format=DATE_FORMAT)


""" MAIN FUNCTION """


def main() -> None:  # pragma: no cover
    """
    main function, parses params and runs command functions
    """

    params = demisto.params()
    command = demisto.command()

    server_url = params.get("server_url")
    if server_url not in ENDPOINT_TO_ZOHO_ACCOUNTS:
        return_error("Invalid URL: Make sure it matches one of the options listed in the help section.")
    client_id = params.get("credentials", {}).get("identifier")
    client_secret = params.get("credentials", {}).get("password")
    client_code = params.get("client_code", {}).get("password")
    verify = not params.get("insecure", False)
    proxy = params.get("proxy", False)
    max_events = int(params.get("max_audit_events", DEFAULT_MAX_FETCH))

    demisto.debug(f"Command being called is {command}")
    try:
        client = Client(
            base_url=server_url,
            client_id=client_id,
            client_secret=client_secret,
            client_code=client_code,
            verify=verify,
            proxy=proxy,
        )
        if command == "test-module":
            raise Exception("Please use !manage-engine-test instead")
        elif command == "manage-engine-test":
            return_results(test_module(client))
        elif command == "manage-engine-get-events":
            return_results(get_events(client, demisto.args()))
        elif command == "fetch-events":
            last_run = demisto.getLastRun()
            next_run, events = fetch_events(
                client=client,
                last_run=last_run,
                max_events_per_fetch=max_events,
            )

            demisto.debug(f"Sending {len(events)} events to XSIAM.")
            send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)
            demisto.debug("Sent events to XSIAM successfully")
            demisto.setLastRun(next_run)
            demisto.debug(f"Setting next run to {next_run}.")

    # Log exceptions and return errors
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{str(e)}")


""" ENTRY POINT """

if __name__ in ("__main__", "__builtin__", "builtins"):
    main()