CitrixDaas

Citrix DaaS simplifies the delivery and management of Citrix technologies.

Analytics & SIEM · Citrix

Details

IDCitrixDaas
ProviderCloud Software Group
CategoryAnalytics & SIEM
From Version6.8.0
Docker Imagedemisto/python3:3.12.13.10116658

README

Citrix DaaS simplifies the delivery and management of Citrix technologies.

Configure Citrix DaaS in Cortex

Parameter Description Required
Server URL   True
Client Id   True
Client Secret   True
Customer ID   True
Site Name   False
Max events per fetch The maximum amount of events to retrieve. This requires the configuration logging database to be configured and enabled. Results are returned in the order of most-recent to least-recent. False
Trust any certificate (not secure)   False
Use system proxy settings   False

Configuration steps

Prerequisites

Get Access to Citrix Cloud

Sign up for a free Citrix Cloud account, or log in to Citrix Cloud.

Citrix Cloud API Access with Service Principals
To create and set up a service principal:

  1. Open the Citrix Cloud console and click the menu icon in the upper-left corner.

  2. Select Identity and Access Management > API Access > Service principals > Create service principal and follow the steps to complete the setup.
    If these options do not appear, you may not have sufficient permissions to manage service principals. Contact your administrator to get the required full access permission.

ServicePrincipals

  1. Add the credentials to your secret management tool as the secret is only displayed once.

  2. Get the Customer ID (a required parameter for the Citrix-CustomerId header).
    a. Log in to the Citrix Cloud.
    b. From the menu, select Identity and Access Management.
    c. Click the API Access tab. You can see the customer ID in the description above the Create Client button.

Locate your tenant’s Citrix Cloud ID

  1. Log in to https://citrix.cloud.com
  2. If you have access to multiple tenants, select the relevant one from the list of tenant names and Citrix Cloud IDs and sign in to it.
    The tenant’s Citrix Cloud ID (for example, ctxtsnaxa) is displayed at the top right corner of the screen.

LoginScreen

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.

citrix-daas-get-events


Extracts Citrix configuration log events. Use with caution during development or debugging; this command may trigger event duplication or exceed API request limits.

Base Command

citrix-daas-get-events

Input

Argument Name Description Required
should_push_events Set to True to create events; otherwise, the command only displays the events. Possible values are: true, false. Default is false. Required
limit The maximum number of logs to return. Default is 10. Optional
search_date_option Time filters for search operations. Optional

Context Output

There is no context output for this command.

Configuration parameters

  • url — Server URL (required)
  • client_id — Client Id (required)
  • credentials — (required)
  • customer_id — Customer ID (required)
  • site_name — Site Name
  • max_fetch — Max events per fetch
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • citrix-daas-get-events

    Extracts Citrix configuration log events. Use with caution during development or debugging; this command may trigger event duplication or exceed API request limits.

import demistomock as demisto
import urllib3
import traceback
from CommonServerPython import *  # noqa # pylint: disable=unused-wildcard-import

# Disable insecure warnings
urllib3.disable_warnings()  # pylint: disable=no-member

""" CONSTANTS """

VENDOR = "Citrix"
PRODUCT = "DaaS"
# max value
RECORDS_REQUEST_LIMIT = 1000
ACCESS_TOKEN_CONST = "access_token"
SITE_ID_CONST = "site_id"
API_RES_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"


""" CLIENT CLASS """


class Client(BaseClient):
    def __init__(
        self, base_url: str, customer_id: str, client_id: str, client_secret: str, site_name: str, proxy: bool, verify: bool
    ):
        self.base_url = base_url
        self.customer_id = customer_id
        self.client_id = client_id
        self.client_secret = client_secret
        self.site_name = site_name
        super().__init__(base_url=base_url, proxy=proxy, verify=verify)

    def request_access_token(self):
        demisto.debug("prepare to create access token")

        headers = {"Accept": "application/json", "Content-Type": "application/x-www-form-urlencoded"}

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

        token_res = self._http_request(
            "post", url_suffix=f"/cctrustoauth2/{self.customer_id}/tokens/clients", headers=headers, data=data
        )

        access_token = token_res.get("access_token")
        if not access_token:
            raise DemistoException("Failed to obtain access token from Citrix DaaS response.")

        demisto.setIntegrationContext({ACCESS_TOKEN_CONST: access_token})
        demisto.debug("access token created")
        return access_token

    def get_site_id(self):
        # get access token value
        integration_context = demisto.getIntegrationContext()
        access_token = integration_context.get(ACCESS_TOKEN_CONST)

        if not access_token:
            access_token = self.request_access_token()

        headers = {"Authorization": f"CwsAuth Bearer={access_token}", "accept": "application/json"}

        url_suffix = f"catalogservice/{self.customer_id}/sites"

        demisto.info(f"Sending http request to get sites with customer id : {self.customer_id}")
        response = self._http_request("get", url_suffix=url_suffix, headers=headers, ok_codes=[200, 401], resp_type="response")

        if response.status_code == 401:
            demisto.info("Access token expired; refreshing...")
            access_token = self.request_access_token()
            headers["Authorization"] = f"CwsAuth Bearer={access_token}"
            demisto.info(f"Sending http request to get sites with customer id : {self.customer_id}")

            res = self._http_request("get", url_suffix=url_suffix, headers=headers)
        else:
            res = response.json()

        sites = res.get("sites", [])
        if not sites:
            raise DemistoException("Failed to obtain sites from Citrix DaaS response.")

        if len(sites) == 1:
            site_id = sites[0].get("id")
        else:
            site_id = next((site.get("id") for site in sites if site.get("displayName") == self.site_name), None)
            if not site_id:
                raise DemistoException(f"Failed to obtain site with the name {self.site_name} from Citrix DaaS response.")
        integration_context[SITE_ID_CONST] = site_id
        demisto.setIntegrationContext(integration_context)
        demisto.debug(f"Site id is {site_id}")
        return site_id

    def get_operations(self, search_date_option: str | None, continuation_token: str = None, limit: int = None, days: int = None):
        # get access token value
        integration_context = demisto.getIntegrationContext()
        access_token = integration_context.get(ACCESS_TOKEN_CONST)
        site_id = integration_context.get(SITE_ID_CONST)

        if not access_token:
            access_token = self.request_access_token()

        if not site_id:
            site_id = self.get_site_id()

        params = assign_params(
            limit=RECORDS_REQUEST_LIMIT,
            continuationToken=continuation_token,
            searchDateOption=search_date_option,
        )
        if days:
            # Note: This parameter is exclusive with parameter searchDateOption.
            # If neither is specified, all records will be returned.
            params.pop("searchDateOption", None)
            params["days"] = days

        # Use the smaller of requested limit or API max per request
        params["limit"] = min(limit if limit else RECORDS_REQUEST_LIMIT, RECORDS_REQUEST_LIMIT)

        headers = {
            "Authorization": f"CwsAuth Bearer={access_token}",
            "accept": "application/json",
            "Citrix-InstanceId": site_id,
            "Citrix-CustomerId": self.customer_id,
        }

        demisto.info(f"Sending http request to get operations with {params=}")
        response = self._http_request(
            "get",
            url_suffix="cvad/manage/ConfigLog/Operations",
            headers=headers,
            params=params,
            ok_codes=[200, 401],
            resp_type="response",
        )

        if response.status_code == 401:
            demisto.info("Access token expired; refreshing...")
            access_token = self.request_access_token()
            headers["Authorization"] = f"CwsAuth Bearer={access_token}"
            demisto.info(f"Sending http request to get operations with {params=}")

            return self._http_request("get", url_suffix="cvad/manage/ConfigLog/Operations", headers=headers, params=params)
        else:
            return response.json()

    def get_operations_with_pagination(
        self,
        limit: int,
        search_date_option: str | None = None,
        last_operation_id: str | None = None,
        days: int | None = None,
        last_run_date: str | None = None,
    ):
        operations: list[dict] = []
        continuation_token = None
        raw_res = None

        while len(operations) < int(limit):
            raw_res = self.get_operations(
                search_date_option=search_date_option, continuation_token=continuation_token, limit=limit, days=days
            )

            items = raw_res.get("Items", [])

            if items and last_run_date:
                res_first_item_time = datetime.strptime(items[0].get("FormattedStartTime"), API_RES_DATE_FORMAT)
                last_fetched_item_time = datetime.strptime(last_run_date, API_RES_DATE_FORMAT)
                if last_fetched_item_time > res_first_item_time:
                    continuation_token = raw_res.get("ContinuationToken")

                    if continuation_token:
                        continue
                    else:
                        break

            items.reverse()

            # get the items after the last fetched record id to avoid duplicates
            if items and last_operation_id:
                for idx, item in enumerate(items):
                    if item.get("Id") == last_operation_id:
                        items = items[idx + 1 :]
                        break

            operations.extend(items)
            continuation_token = raw_res.get("ContinuationToken")

            if not continuation_token:
                break

        operations = operations[:limit]

        for operation in operations:
            operation["_time"] = operation.get("FormattedStartTime")
        return operations, raw_res


""" HELPER FUNCTIONS """


def get_events_command(client: Client, args: dict):  # type: ignore
    limit = int(args.get("limit", "10"))
    search_date_option = args.get("search_date_option")
    days = args.get("days")

    should_push_events = argToBoolean(args.get("should_push_events", False))

    demisto.debug(f"Running citrix-daas-get-events with {should_push_events=}")

    operations, raw_res = client.get_operations_with_pagination(limit=limit, search_date_option=search_date_option, days=days)

    results = CommandResults(
        outputs_prefix="CitrixDaas.Event",
        outputs_key_field="Id",
        outputs=operations,
        readable_output=tableToMarkdown("Events List", operations),
        raw_response=raw_res,
    )

    if should_push_events:
        demisto.debug(f"send {len(operations)} events to xsiam")
        send_events_to_xsiam(operations, vendor=VENDOR, product=PRODUCT)

    return results


def days_since(timestamp_str) -> int:
    """Returns 0 if the timestamp is less than one hour old; otherwise,
    returns the day difference (1 = today, 2 = yesterday, etc.)."""
    # Parse the ISO-8601 timestamp with Zulu time (UTC)
    dt = datetime.strptime(timestamp_str, API_RES_DATE_FORMAT)
    dt = dt.replace(tzinfo=timezone.utc)

    # Current time in UTC
    now = datetime.now(timezone.utc)

    delta = now - dt
    # Difference in minutes
    minutes = delta.total_seconds() / 60
    # Difference in days
    days = delta.days

    if minutes < 60:
        return 0

    if days <= 0:
        return 1

    return delta.days + 1


def deduplicate_events(events: list[dict[str, Any]], last_fetched_ids: list[str]) -> list[dict[str, Any]]:
    """Remove already-processed events based on previously fetched IDs."""

    if not last_fetched_ids:
        demisto.debug("[Dedup] No deduplication needed (first run - no previous IDs)")
        return events

    demisto.debug(f"[Dedup] Checking {len(events)} events against {len(last_fetched_ids)} previously fetched IDs")

    # Convert to set for O(1) lookup
    fetched_ids_set = set(last_fetched_ids)

    # Filter out events that were already fetched
    new_events = [event for event in events if event.get("Id") not in fetched_ids_set]

    skipped_count = len(events) - len(new_events)
    if skipped_count > 0:
        demisto.debug(f"[Dedup] Skipped {skipped_count} duplicates. {len(new_events)} new events remain.")
    else:
        demisto.debug("[Dedup] No duplicates found.")

    return new_events


def fetch_events_command(client: Client, max_fetch: int, last_run: dict):
    last_run_date = last_run.get("LastRun")
    last_fetched_ids = last_run.get("LastFetchedIds", [])
    # assuming the sending order is ascending
    last_operation_id = last_fetched_ids[-1] if last_fetched_ids else None

    if not last_run_date:
        operations, _ = client.get_operations_with_pagination(
            limit=max_fetch, last_operation_id=last_operation_id, search_date_option="LastMinute", last_run_date=last_run_date
        )
    else:
        days = days_since(last_run_date)

        if days == 0:
            operations, _ = client.get_operations_with_pagination(
                limit=max_fetch, last_operation_id=last_operation_id, search_date_option="LastHour", last_run_date=last_run_date
            )
        else:
            operations, _ = client.get_operations_with_pagination(
                limit=max_fetch, last_operation_id=last_operation_id, days=days, last_run_date=last_run_date
            )

    if operations:
        # Deduplicate
        operations = deduplicate_events(operations, last_fetched_ids)
        new_last_run = operations[-1]["_time"]

        ids_at_last_timestamp = [
            operation.get("Id") for operation in operations if operation.get("_time") == new_last_run and operation.get("Id")
        ]

        last_run = {"LastRun": new_last_run, "LastFetchedIds": ids_at_last_timestamp}

    return operations, last_run


def module_test_command(client: Client, args: dict):
    get_events_command(client, args)
    return "ok"


""" MAIN FUNCTION """


def main():
    command = demisto.command()
    params = demisto.params()
    args = demisto.args()

    demisto.debug(f"Command being called is {command}")

    try:
        client = Client(
            base_url=params.get("url"),
            customer_id=params.get("customer_id"),
            client_id=params.get("client_id"),
            client_secret=params.get("credentials", {}).get("password"),
            site_name=params.get("site_name", ""),
            verify=not params.get("insecure"),
            proxy=params.get("proxy"),
        )

        if command == "test-module":
            result = module_test_command(client, args)
            return_results(result)

        elif command == "citrix-daas-get-events":
            results = get_events_command(client, args)
            return_results(results)

        elif command == "fetch-events":
            max_fetch = int(params.get("max_fetch", "2000"))
            last_run = demisto.getLastRun()
            demisto.debug(f"last run is: {last_run}")

            events, last_run = fetch_events_command(client, max_fetch, last_run)

            if not events:
                demisto.info("No events found")
            demisto.debug(f"send {len(events)} events to xsiam")
            send_events_to_xsiam(events, vendor=VENDOR, product=PRODUCT)
            demisto.setLastRun(last_run)
            demisto.debug(f"Last run set to: {last_run}")
        else:
            raise NotImplementedError(f"Command {command} is not implemented")

    except Exception as e:
        demisto.error(f"{type(e).__name__} in {command}: {str(e)}\nTraceback:\n{traceback.format_exc()}")
        return_error(f"Failed to execute {command} command.\nError:\n{e}")


""" ENTRY POINT """

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