Cisco AppDynamics

AppDynamics enables you to automate incident management, gain real-time performance metrics, and optimize applications to meet business needs.

Analytics & SIEM · Cisco AppDynamics

Details

IDCisco AppDynamics
ProviderCisco Systems
CategoryAnalytics & SIEM
From Version8.4.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesXSIAM

README

AppDynamics enables you to automate incident management, gain real-time performance metrics, and optimize applications to meet business needs.

API Clients

To create API clients, you are required to be assigned to the role of an Account Owner or an Administer. You can view the API Client settings in the Settings > Administration page of the Controller.

How to Create API Clients

You can create new API Client identity types that can be used to generate OAuth tokens.

  1. Log in to the Controller UI as an Account Owner or other roles with the Administer users, groups, roles, permission.
  2. Click > Administration.
  3. Click the API Clients tab to view the list of existing clients.
  4. Click + Create.
  5. Enter the Client Name and Description.
  6. Click Generate Secret to populate the Client Secret.
  7. This will generate a UUID as the secret of the API Client.
  8. Set the Default API-generated Token Expiration. This expiration only applies to authentication tokens generated through the /controller/api/oauth/access_token REST API, not to Temporary Access Tokens generated from the UI. See Using the Access Token.
  9. Add the Roles you would like to associate with this API Client. You can add or remove roles at any time.
  10. Click Save at the top right.

Application ID

You can find the application_id in the Controller UI:

  1. Navigate to Applications in the AppDynamics Controller.
  2. Select the required application.
  3. The application ID is visible in the URL as application = ID.

Configure Cisco AppDynamics on Cortex XSOAR

  1. Navigate to Settings > Integrations > Servers & Services.
  2. Search for Cisco AppDynamics.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Required
    Server URL True
    Client ID True
    Client Secret True
    Application ID True
    Trust any certificate (not secure) False
    Use system proxy settings False
    Fetch Events False
    Event types to fetch (Multi-select list) False
    The maximum number of Audit History API per fetch False
    The maximum number of Healthrule Violations Events per fetch False
  4. Click Test to validate the URLs, credentials, and connection.

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-appdynamics-get-events


Retrieves a list of events from the cisappdynamics instance.

Base Command

cisco-appdynamics-get-events

Input

Argument Name Description Required
should_push_events Set this argument to True in order to create events, otherwise it will only display them. Possible values are: True, False. Required
limit The maximum number of events to fetch. Default is 1000. Optional
events_type_to_fetch Which events to fetch. Possible values are: Audit, Healthrule Violations Events. Default is Audit,Healthrule Violations Events. Optional
start_date The starting date from which events should be fetched. The date should be in the format “YYYY-MM-DDTHH:MM:SS”. Example: 2025-01-01T11:27:08. Optional
end_date The date up to which events should be fetched. The date should be in the format “YYYY-MM-DDTHH:MM:SS”. Example: 2025-01-01T11:27:08. Optional

Configuration parameters

  • url — Server URL (required)
  • credentials — Client ID (required)
  • application_id — Application ID (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • isFetchEvents — Fetch Events
  • events_type_to_fetch — Event types to fetch (Multi-select list) (required)
  • max_audit_fetch — The maximum number of Audit History API per fetch
  • max_healthrule_fetch — The maximum number of Health Rule Violations Events per fetch

Commands (1)

  • cisco-appdynamics-get-events

    Retrieves a list of events from the cisappdynamics instance.

import demistomock as demisto
from CommonServerPython import *
import urllib3
from dateutil import parser

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """
VENDOR = "cisco"
PRODUCT = "appdynamics"

DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"


class EventType:
    """
    This class defines a CybelAngel API Event - used to dynamically store
    per-type settings for fetching and deduplicating events.
    """

    def __init__(
        self,
        name: str,
        url_suffix: str,
        time_field: str,
        max_fetch: int,
        source_log_type: str,
        api_limit: int = 0,
        default_params: dict = {},
    ):
        """
        Args:
            name            (str): Human-friendly name of the event type.
            url_suffix      (str): URL suffix of the CybelAngel API endpoint (no leading slash).
            time_field      (str): Field name in the event used for timestamp mapping (`_time`).
            max_fetch       (int): Default value for the maximum number of events to fetch.
            source_log_type (str): Value to assign to each event's `source_log_type` field in XSIAM.
            api_limit       (int): Maximum events that the API can retrieve in one call.
            default_params (dict): Dict to contain default parameters for the API calls.
        """
        self.name = name
        self.url_suffix = url_suffix
        self.max_fetch = max_fetch
        self.time_field = time_field
        self.source_log_type = source_log_type
        self.api_limit = api_limit
        self.default_params = default_params


""" CLIENT CLASS """

AUDIT = EventType(
    name="Audit",
    url_suffix="/ControllerAuditHistory",
    time_field="timeStamp",
    max_fetch=3000,
    source_log_type="Audit History",
)

HEALTH_EVENT = EventType(
    name="Health Rule Violations",
    url_suffix="/rest/applications/application_id/problems/healthrule-violations",
    time_field="startTimeInMillis",
    max_fetch=3000,
    source_log_type="Health Rule Violations",
    api_limit=600,
    default_params={
        "time-range-type": "BETWEEN_TIMES",
        "output": "JSON",
    },
)

EVENT_TYPES = {"Audit": AUDIT, "Health Rule Violations": HEALTH_EVENT}


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

    def __init__(
        self,
        base_url: str,
        client_id: str,
        client_secret: str,
        application_id: str,
        verify: bool,
        proxy: bool,
    ) -> None:
        """
        Initializes the Client.
        """
        super().__init__(base_url=base_url, verify=verify, headers={}, proxy=proxy)
        self.client_id = client_id
        self.client_secret = client_secret
        self.application_id = application_id

    def create_access_token(self) -> str:
        """
        Generates a new access token via the OAuth API.
        Stores the token and computes its expiry time.

        Returns:
            str: The new access token.
        """
        demisto.debug("Requesting new access token from AppDynamics OAuth API")

        headers = {"Content-Type": "application/x-www-form-urlencoded"}
        params = {"grant_type": "client_credentials", "client_id": self.client_id, "client_secret": self.client_secret}

        response = self._http_request(method="POST", url_suffix="/api/oauth/access_token", params=params, headers=headers)

        access_token = response.get("access_token")
        expires_in = response.get("expires_in")
        demisto.debug(f"Access token expire in: {expires_in}")
        token_expiry = datetime.now(timezone.utc) + timedelta(seconds=int(expires_in) - 15)

        demisto.debug(f"Access token obtained, expires at {token_expiry.isoformat()}")
        set_integration_context({"access_token": access_token, "token_expiry": token_expiry.isoformat()})
        return access_token

    def get_access_token(self) -> str:
        """
        Returns a valid access token, generating a new one if necessary.
        """
        integration_context = get_integration_context()

        access_token = integration_context.get("access_token", "")
        token_expiry = integration_context.get("token_expiry", "")

        if not access_token or not token_expiry:
            demisto.debug("Token doesn't exists, requesting new token.")
            return self.create_access_token()

        if datetime.now(timezone.utc) >= datetime.fromisoformat(token_expiry):
            demisto.debug("Token Expired, requesting new token.")
            return self.create_access_token()

        demisto.debug(f"Using cached access token. Expires at {token_expiry}")
        return access_token

    def authorized_request(self, url_suffix: str, params: dict) -> list[dict]:
        """
        Wrapper for _http_request() that validate the token then adds it to the headers.
        Adds output="JSON" to params to return JSON format.
        Args:
            url_suffix (str): the url_suffix as described in the API docs foreach request type.
            params (Dict): hold time interval and additional params for the request.
        Return:
            List[Dict]: JSON-decoded list of events.
        """
        token = self.get_access_token()
        headers = {"Authorization": f"Bearer {token}"}
        params["output"] = "JSON"
        response = self._http_request(
            method="GET",
            url_suffix=url_suffix,
            params=params,
            headers=headers,
            resp_type="json",
        )
        return response

    def get_audit_logs(self, start_time: str, end_time: str) -> list[dict]:
        """
        Fetches audit-history events.
        API return all events from start_dt to end_dt include start and end.
        API return in ascending order.
        Args:
            start_dt (datetime): start time in the right format: '2015-12-19T10:50:03.607-0000'.
            end_dt (datetime):   end time in the right format: '2015-12-19T10:50:03.607-0000'.
        Returns:
            List[Dict]: JSON-decoded list of audit events.
        """
        params = {
            "startTime": start_time,
            "endTime": end_time,
        }
        demisto.debug(f"Fetching audit logs from {start_time} to {end_time}")

        events = self.authorized_request(
            url_suffix=AUDIT.url_suffix,
            params=params,
        )
        demisto.debug(f"Received {len(events)} audit logs from API.")

        return add_fields_to_events(events, AUDIT)

    def get_health_events(self, start_time: int, end_time: int) -> list[dict]:
        """
        Fetches all Health Rule Violations in a loop,
        using the API's BETWEEN_TIMES.
        start-time and end-time are in milliseconds.
        Args:
            from_date(int): timestamp.
            to_date  (int): timestamp.
        Returns:
            list[Dict]: list of all events.
        """
        events: list[dict] = []
        params = HEALTH_EVENT.default_params.copy()
        demisto.debug("Fetching Health Rule Violations")
        while len(events) <= HEALTH_EVENT.max_fetch:
            params.update(
                {
                    "start-time": start_time,
                    "end-time": end_time,
                }
            )
            batch = self.authorized_request(url_suffix=HEALTH_EVENT.url_suffix, params=params)
            demisto.debug(f"Fetched {len(batch)} events successfully.")
            if not batch:
                demisto.debug("No events fetched from API")
                break
            events.extend(batch)
            if len(batch) < HEALTH_EVENT.api_limit:
                demisto.debug(f"Fetched {len(batch)} events from API < API Limit {HEALTH_EVENT.api_limit} on the last page.")
                break
            start_time = events[-1][HEALTH_EVENT.time_field]

        demisto.debug(f"Fetched total {len(events)} Health Rule Violations from API.")
        return add_fields_to_events(events, HEALTH_EVENT)


def fetch_events(
    client: Client,
    events_type_to_fetch: list[EventType],
) -> tuple[list[dict], dict]:
    """
    Fetch events from the client based on the last run times and event types specified.

    Args:
        client (Client): The client instance to fetch events from.
        fetch_types (list[EventType]): A list of event types to fetch.

    Returns:
        tuple[List[Dict], Dict]: A tuple containing a list of fetched events and a dictionary with the next run times.
    """

    all_events = []
    current_time = int(time.time() * 1000)
    last_run = get_last_run(current_time, events_type_to_fetch)
    demisto.debug(f"fetch_events::Current time: {current_time}")

    event_fetch_function = {
        AUDIT.name: client.get_audit_logs,
        HEALTH_EVENT.name: client.get_health_events,
    }

    for event_type in events_type_to_fetch:
        demisto.debug(f"Fetching {event_type.name}")
        start_time = last_run[event_type.name]  # in timestamp
        demisto.debug(f"Last run {start_time}")
        events = event_fetch_function.get(event_type.name)(  # type: ignore
            start_time=timestamp_to_api_format(start_time, event_type),
            end_time=timestamp_to_api_format(current_time, event_type),
        )
        demisto.debug(f"Fetched {len(events)} events")
        if events:
            events = events[: event_type.max_fetch]
            all_events.extend(events)
            last_run[event_type.name] = int(events[-1][event_type.time_field]) + 1
        else:
            last_run[event_type.name] = current_time + 1

    demisto.debug(f"Total events fetched: {len(all_events)}")
    return all_events, last_run


def add_fields_to_events(events: list[dict], event_type: EventType) -> list[dict]:
    """
    Enriches each event dict with XSIAM fields, based on the provided source type.
    Args:
        events:     List of event dicts to enrich.
        event_type: The event type.
    Returns:
        The same list, with each dict now having:
          - '_time'           (int) timestamp taken from the correct field
          - 'SOURCE_LOG_TYPE' (str) set to event_type.source_log_type
    """
    if not events:
        return []
    for event in events:
        event["_time"] = timestamp_to_datestring(event[event_type.time_field], DATE_FORMAT, is_utc=True)
        event["SOURCE_LOG_TYPE"] = event_type.source_log_type

    return events


def test_module_command(client: Client, params: dict):  # pragma: no cover
    """
    Tests API connectivity and authentication
    When 'ok' is returned it indicates the integration works like it is supposed to and connection to the service is
    successful.
    Raises exceptions if something goes wrong.

    Args:
        client (Client): Cisco AppDynamics client to use.

    Returns:
        str: 'ok' if test passed, anything else will raise an exception and will fail the test.
    """
    fetch_events(client, set_event_type_fetch_limit(params))
    return "ok"


def get_events(client: Client, args: dict[str, Any], params: dict[str, Any]) -> CommandResults:
    """
    A test‐driven version of fetch_events.

    Args (in `args` dict):
      - start_time (str): ISO 8601 string, e.g. "2025-04-27T10:00:00Z"
      - end_time   (str): ISO 8601 string, e.g. "2025-04-27T10:05:00Z"
      - limit      (str): comma-separated list of event types to fetch,
                         e.g. "Audit, Health Rule Violations".
      - should_push_events (bool): push events to XSIAM.

    Returns:
      - CommandResults: include human readable from the events.
    """
    event_type_name = args.get("events_type_to_fetch", AUDIT.name)
    if event_type_name == HEALTH_EVENT.name:
        HEALTH_EVENT.url_suffix = f"/rest/applications/{params['application_id']}/problems/healthrule-violations"
    limit = int(args.get("limit", 50))
    now = datetime.now(timezone.utc)
    end_time = parser.parse(args.get("end_date", "")) if args.get("end_date", "") else now
    start_time = parser.parse(args.get("start_date", "")) if args.get("start_date", "") else (now - timedelta(days=1))
    demisto.debug(f"Get events from {start_time} to {end_time}")
    event_fetch_function = {
        AUDIT.name: client.get_audit_logs,
        HEALTH_EVENT.name: client.get_health_events,
    }

    fetch_func = event_fetch_function[event_type_name]
    events = fetch_func(  # type: ignore
        start_time=datetime_to_api_format(start_time, EVENT_TYPES[event_type_name]),
        end_time=datetime_to_api_format(end_time, EVENT_TYPES[event_type_name]),
    )

    events = events[:limit]
    if argToBoolean(args.get("is_fetch_events", False)):
        send_events_to_xsiam(vendor=VENDOR, product=PRODUCT, events=events)
        demisto.debug(f"Successfully send {len(events)} to XSIAM.")
    return CommandResults(
        readable_output=tableToMarkdown(f"{event_type_name}", events),
    )


def timestamp_to_api_format(time: int, eventType: EventType) -> str | int:
    if eventType.name == AUDIT.name:
        ts_sec = time / 1000.0
        dt = datetime.fromtimestamp(ts_sec, tz=timezone.utc)
        return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{dt.microsecond // 1000:03d}" + "-0000"

    elif eventType.name == HEALTH_EVENT.name:
        return time
    return ""


def datetime_to_api_format(dt: datetime, eventType: EventType) -> str | int:
    if eventType.name == AUDIT.name:
        return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{dt.microsecond // 1000:03d}-0000"
    elif eventType.name == HEALTH_EVENT.name:
        return int(dt.timestamp() * 1000)
    return ""


def get_last_run(now: int, events_type_to_fetch: list[EventType]) -> dict[str, int]:
    """
    Retrieve and initialize the “last run” timestamps for a set of event types.
    This function loads the existing last‐run state via `demisto.getLastRun()`.
    For any event type that is missing or newly requested, it sets:
      - `EVENT_TYPES.name` to one minute before `now`.
    Args:
        now (datetime): Reference time for computing initial fetch timestamps.
        events_to_fetch (List[str]): Names of event types that should be tracked this run.
    Returns:
        Dict[str, Dict[str, Any]]: A mapping of each event type to its last-run info:
            {
                "AUDIT.name": timestamp int..
                "HEALTH.name" timestamp int.
            }
    """
    raw_last_run = demisto.getLastRun() or {}
    last_run = {}

    for event_type in EVENT_TYPES.values():
        last_time_iso = raw_last_run.get(event_type.name)
        if last_time_iso and event_type in events_type_to_fetch:
            last_run[event_type.name] = last_time_iso
        else:
            last_run[event_type.name] = now - (60 * 1000)

    return last_run


def set_event_type_fetch_limit(params: dict[str, Any]) -> list[EventType]:
    """
    Parses the event types to fetch from parameters and returns a dictionary mapping
    each selected event type's suffix to its corresponding max fetch limit.
    Args:
        params (Dict[str, Any]): Integration parameters.
    Returns:
        list[EventType]: List of event type to fetch from the api call.
    """
    application_id = params.get("application_id", "")
    event_types_to_fetch = [et.strip() for et in argToList(params.get("events_type_to_fetch", [AUDIT.name, HEALTH_EVENT.name]))]
    demisto.debug(f"List:{event_types_to_fetch}, list length:{len(event_types_to_fetch)}")
    fetch_limits = {
        AUDIT.name: arg_to_number(params.get("max_audit_fetch")) or AUDIT.max_fetch,
        HEALTH_EVENT.name: arg_to_number(params.get("max_healthrule_fetch")) or HEALTH_EVENT.max_fetch,
    }
    event_types = []
    for event_type in EVENT_TYPES.values():
        if event_type.name in event_types_to_fetch:
            event_type.max_fetch = fetch_limits[event_type.name]
            event_types.append(event_type)

    if HEALTH_EVENT.name in event_types_to_fetch:
        HEALTH_EVENT.url_suffix = f"/rest/applications/{application_id}/problems/healthrule-violations"

    return event_types


""" MAIN FUNCTION """


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

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

    base_url = params.get("url", "")
    client_id = params.get("credentials", {}).get("identifier")
    client_secret = params.get("credentials", {}).get("password")
    application_id = params.get("application_id", "")
    verify = argToBoolean(not params.get("insecure", False))
    proxy = argToBoolean(params.get("proxy", False))

    demisto.debug(f"Command being called is {command}")
    try:
        client = Client(
            base_url=base_url,
            client_id=client_id,
            client_secret=client_secret,
            application_id=application_id,
            verify=verify,
            proxy=proxy,
        )

        if command == "test-module":
            return_results(test_module_command(client, params))

        elif command == "fetch-events":
            events, next_run = fetch_events(
                client,
                set_event_type_fetch_limit(params),
            )
            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.debug(
                f"setLastRun going to set: Audit:{next_run.get(AUDIT.name,'')}, Health: {next_run.get(HEALTH_EVENT.name,'')}"
            )
            demisto.setLastRun(next_run)
            demisto.debug(f"Setting next run to {next_run}.")

        elif command == "cisco-appdynamics-get-events":
            return_results(get_events(client, args, params))

    except Exception as e:
        error_message = str(e)
        if "Verify that the server URL" in error_message or "Not Found" in error_message:
            return_error(
                f"Failed to execute {command} command.\n"
                "Error:\n"
                "Verify that the server URL parameter is correct and that you have access to the server from your host."
            )
        if "Unauthorized" in error_message:
            return_error(
                f"Failed to execute {command} command.\n"
                "Error:\n"
                "Verify that the server URL parameter and credentials are correct "
                "and that you have access to the server from your host."
            )
        if "Invalid application id" in error_message:
            return_error(f"Failed to execute {command} command.\nError:\nInvalid application id is specified.")
        return_error(f"Failed to execute {command} command.\nError:\n{error_message}")


""" ENTRY POINT """

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