ProofpointThreatResponseEventCollector

Use the Proofpoint Threat Response integration to orchestrate and automate incident response.

Analytics & SIEM · Proofpoint Threat Response

Details

IDProofpointThreatResponseEventCollector
ProviderThoma Bravo
CategoryAnalytics & SIEM
From Version6.8.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM EDR Cortex Cloud Cloud Runtime Security

README

Use the Proofpoint Threat Response integration to orchestrate and automate incident response.

This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.

Configure Proofpoint Threat Response Event Collector in Cortex

Parameter Description Required
Server URL (e.g., https://192.168.0.1)   True
API Key for the authentication.   True
Trust any certificate (not secure)   False
Use system proxy settings   False
First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days) The time range for the initial data fetch. If timeout errors occur, consider changing this value. False
Fetch limit - maximum number of incidents per fetch   False
Fetch delta - The delta time in each batch. e.g. 1 hour, 3 minutes. The time range between create_after and created_before that is sent to the API when fetching older incidents. If timeout errors occur, consider changing this value. False
Fetch incidents with specific event sources. Can be a list of comma-separated values.   False
Fetch incidents with specific ‘Abuse Disposition’ values. Can be a list of comma-separated values.   False
Fetch incident with specific states.   False
POST URL of the JSON alert source. You can find this value by navigating to Sources -&gt; JSON event source -&gt; POST URL. 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.

proofpoint-trap-get-events


Retrieves all incident metadata from Threat Response by specifying filter criteria such as the state of the incident or time of closure.

Base Command

proofpoint-trap-get-events

Input

Argument Name Description Required
should_push_events If true, the command will create events, otherwise it will only display them. Possible values are: true, false. Default is false. Required
state The state of the incidents to retrieve. Possible values are: new, open, assigned, closed, ignored. Optional
created_after Retrieve incidents that were created after this date, in ISO 8601 format (UTC). Example: 2020-02-22 or 2020-02-22T00:00:00Z. Optional
created_before Retrieve incidents that were created before this date, in ISO 8601 format (UTC). Example: 2020-02-22 or 2020-02-22T00:00:00Z. Optional
closed_after Retrieve incidents that were closed after this date, in ISO 8601 format (UTC). Example: 2020-02-22 or 2020-02-22T00:00:00Z. Optional
closed_before Retrieve incidents that were closed before this date, in ISO 8601 format (UTC). Example: 2020-02-22 or 2020-02-22T00:00:00Z. Optional
expand_events If false, will return an array of event IDs instead of full event objects. This will significantly speed up the response time of the API for incidents with a large number of alerts. Possible values are: true, false. Optional
limit The maximum number of incidents to return. Default is 100. Required

Context Output

There is no context output for this command.

Configuration parameters

  • url — Server URL (e.g., https://192.168.0.1) (required)
  • credentials — (required)
  • first_fetch — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • fetch_limit — Fetch limit - maximum number of incidents per fetch
  • fetch_delta — Fetch delta - The delta time in each batch. e.g., 1 hour, 3 minutes.
  • event_sources — Fetch incidents with specific event sources. Can be a list of comma-separated values.
  • abuse_disposition — Fetch incidents with specific 'Abuse Disposition' values. Can be a list of comma-separated values.
  • states — Fetch incident with specific states.
  • post_url_id — POST URL of the JSON alert source.
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • proofpoint-trap-get-events

    Retrieves all incident metadata from Threat Response by specifying filter criteria such as the state of the incident or time of closure.

import copy
from datetime import timedelta

import demistomock as demisto  # noqa: F401

# Disable insecure warnings
import urllib3
from CommonServerPython import *  # noqa: F401

urllib3.disable_warnings()

""" CONSTANTS """
TIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
PRODUCT = "threat_response"
VENDOR = "proofpoint"
MAX_API_REQUESTS = 50


class Client(BaseClient):
    """
    Client will implement the service API, and should not contain any logic.
    Should only do requests and return data.
    """

    def get_incidents_request(self, query_params):
        """Perform an API request to get incidents from ProofPoint.

        Args:
            query_params(dict): The params of the request

        Returns:
            list. The incidents returned from the API call
        """
        raw_response = self._http_request(
            method="GET",
            url_suffix="api/incidents",
            params=query_params,
        )
        return raw_response


def test_module(client, first_fetch):
    """Perform API call to check that the API is accessible.

    Returns:
        'ok' if test passed, anything else will fail the test.
    """
    query_params = {"created_after": first_fetch, "state": "open"}

    client.get_incidents_request(query_params)
    return "ok"


def create_incidents_human_readable(human_readable_message, incidents_list):
    """Creates the human readable entry for incidents

    Args:
        human_readable_message (str): The title of the human readable table
        incidents_list (list): The incidents list to insert to the table

    Returns:
        str. The incidents human readable in markdown format
    """
    human_readable = []
    human_readable_headers = [
        "ID",
        "Created At",
        "Type",
        "Summary",
        "Score",
        "Event Count",
        "Assignee",
        "Successful Quarantines",
        "Failed Quarantines",
        "Pending Quarantines",
    ]
    for incident in incidents_list:
        human_readable.append(
            {
                "Created At": incident.get("created_at"),
                "ID": incident.get("id"),
                "State": incident.get("state"),
                "Type": incident.get("type"),
                "Summary": incident.get("summary"),
                "Score": incident.get("score"),
                "Event Count": incident.get("event_count"),
                "Assignee": incident.get("assignee"),
                "Successful Quarantines": incident.get("successful_quarantine"),
                "Failed Quarantines": incident.get("failed_quarantines"),
                "Pending Quarantines": incident.get("pending_quarantines"),
            }
        )

    return tableToMarkdown(human_readable_message, human_readable, human_readable_headers, removeNull=True)


def list_incidents_command(client, args):
    """Retrieves incidents from ProofPoint API"""
    limit = arg_to_number(args.pop("limit"))

    raw_response = client.get_incidents_request(args)

    incidents_list = raw_response[:limit]
    events = get_events_from_incidents(incidents_list)
    human_readable = create_incidents_human_readable("List Incidents Results:", events)

    return events, human_readable, raw_response


def pass_sources_list_filter(incident, sources_list):
    """Checks whether the event sources of the incident contains at least one of the sources in the sources list.

    Args:
        incident (dict): The incident to check
        sources_list (list): The list of sources from the customer

    Returns:
        bool. Whether the incident has passed the filter or not
    """
    if len(sources_list) == 0:
        return True

    return any(source in incident.get("event_sources") for source in sources_list)


def pass_abuse_disposition_filter(incident, abuse_disposition_values):
    """Checks whether the incident's 'Abuse Disposition' value is in the abuse_disposition_values list.

    Args:
        incident (dict): The incident to check
        abuse_disposition_values (list): The list of relevant values from the customer

    Returns:
        bool. Whether the incident has passed the filter or not
    """
    if len(abuse_disposition_values) == 0:
        return True

    for incident_field in incident.get("incident_field_values", []):
        if incident_field["name"] == "Abuse Disposition" and incident_field["value"] in abuse_disposition_values:
            return True

    return False


def filter_incidents(incidents_list):
    """Filters the incidents list by 'abuse disposition' and 'source list' values

    Args:
        incidents_list (list): The incidents list to filter

    Returns:
        list. The filtered incidents list
    """
    filtered_incidents_list = []
    params = demisto.params()
    sources_list = argToList(params.get("event_sources"))
    abuse_disposition_values = argToList(params.get("abuse_disposition"))

    if not sources_list and not abuse_disposition_values:
        return incidents_list

    for incident in incidents_list:
        if pass_sources_list_filter(incident, sources_list) and pass_abuse_disposition_filter(incident, abuse_disposition_values):
            filtered_incidents_list.append(incident)

    return filtered_incidents_list


def get_time_delta(fetch_delta):
    """Gets the time delta from a string that is combined with a number and a string of (minute/hour)
    Args:
        fetch_delta(str): The fetch delta param.
    Returns:
        The time delta.
    """
    fetch_delta_split = fetch_delta.strip().split(" ")
    if len(fetch_delta_split) != 2:
        raise Exception("The fetch_delta is invalid. Please make sure to insert both the number and the unit of the fetch delta.")

    unit = fetch_delta_split[1].lower()
    number = int(fetch_delta_split[0])

    if unit not in [
        "minute",
        "minutes",
        "hour",
        "hours",
    ]:
        raise Exception('The unit of fetch_delta is invalid. Possible values are "minutes" or "hours".')

    if "hour" in unit:
        time_delta = timedelta(hours=number)  # batch by hours
    else:
        time_delta = timedelta(minutes=number)  # batch by minutes
    return time_delta


def get_new_incidents(client, request_params, last_fetched_id):
    """Perform an API request to get incidents from ProofPoint , filters then according to params, order them and
    return only the new incidents.

    As the api does not return the results in an specific order, we query the api on specific time frames using
    created_before and created_after using the fetch delta parameter.
    Args:
        request_params(dict): The params of the request
        last_fetched_id(int): The ID of the last incident that was fetched in the previous fetch.
    Returns:
        list. The incidents returned from after the necessary actions.
    """
    incidents = client.get_incidents_request(request_params)
    filtered_incidents_list = filter_incidents(incidents)
    ordered_incidents = sorted(filtered_incidents_list, key=lambda k: (k["created_at"], k["id"]))
    return list(filter(lambda incident: int(incident.get("id")) > last_fetched_id, ordered_incidents))


def get_incidents_batch_by_time_request(client, params):
    """Perform an API request to get incidents from ProofPoint in batches to prevent a timeout.

    As the api does not return the results in an specific order, we query the api on specific time frames using
    created_before and created_after using the fetch delta parameter.
    Args:
        params(dict): The params of the request

    Returns:
        list. The incidents returned from the API call
    """
    incidents_list = []  # type:list

    fetch_delta = params.get("fetch_delta", "6 hours")
    fetch_limit = arg_to_number(params.get("fetch_limit", "100"))
    last_fetched_id = arg_to_number(params.get("last_fetched_id", "0"))

    current_time = datetime.now()

    time_delta = get_time_delta(fetch_delta)

    created_after = datetime.strptime(params.get("created_after"), TIME_FORMAT)
    created_before = created_after + time_delta

    request_params = {
        "state": params.get("state"),
        "created_after": created_after.isoformat().split(".")[0] + "Z",
        "created_before": created_before.isoformat().split(".")[0] + "Z",
    }

    demisto.debug(
        f"[BATCH_START] Starting get_incidents_batch_by_time_request with state={params.get('state')}, "
        f"fetch_limit={fetch_limit}, last_fetched_id={last_fetched_id}, fetch_delta={fetch_delta}, "
        f"created_after={request_params['created_after']}, current_time={current_time.isoformat()}"
    )

    iteration_count = 0

    # Simplified loop: fetch until we reach current_time OR hit fetch_limit
    while created_after < current_time and len(incidents_list) < fetch_limit:  # type: ignore[operator]
        iteration_count += 1

        if iteration_count > MAX_API_REQUESTS:
            demisto.debug(
                f"[BATCH_API_LIMIT] Reached maximum API request limit of {MAX_API_REQUESTS}. "
                f"Stopping batch processing with {len(incidents_list)} incidents collected so far. "
                f"created_after={created_after.isoformat()}, current_time={current_time.isoformat()}"
            )
            break

        # Set created_before to the minimum of (created_after + time_delta) or current_time
        # This ensures we always fetch up to current_time in the last iteration
        created_before = min(created_after + time_delta, current_time)

        request_params["created_after"] = created_after.isoformat().split(".")[0] + "Z"
        request_params["created_before"] = created_before.isoformat().split(".")[0] + "Z"

        demisto.debug(
            f"[BATCH_LOOP_START] Iteration #{iteration_count}: fetch_limit={fetch_limit}, "
            f"current_incidents_count={len(incidents_list)}, "
            f"created_after={request_params['created_after']}, "
            f"created_before={request_params['created_before']}"
        )

        demisto.debug(f"[API_CALL_START] Iteration #{iteration_count}: Calling get_new_incidents...")
        new_incidents = get_new_incidents(client, request_params, last_fetched_id)
        demisto.debug(f"[API_CALL_END] Iteration #{iteration_count}: get_new_incidents returned {len(new_incidents)} incidents")

        incidents_list.extend(new_incidents)

        demisto.debug(
            f"[BATCH_LOOP_END] Iteration #{iteration_count}: total_incidents={len(incidents_list)}, "
            f"new_incidents_added={len(new_incidents)}"
        )

        # Advance to next time window
        created_after = created_before

    demisto.debug(
        f"[BATCH_COMPLETE] Batch processing completed after {iteration_count} iterations, "
        f"total_incidents={len(incidents_list)}, fetch_limit={fetch_limit}, "
        f"reached_current_time={created_after >= current_time}, "
        f"hit_api_limit={iteration_count > MAX_API_REQUESTS}"
    )

    incidents_list_limit = incidents_list[:fetch_limit]

    # Return the final created_after so the caller can persist progress even when no incidents were found
    final_created_after = created_after.strftime(TIME_FORMAT)

    demisto.debug(
        f"[BATCH_END] get_incidents_batch_by_time_request completed, "
        f"total_iterations={iteration_count}, final_incident_count={len(incidents_list_limit)}, "
        f"incidents_truncated={len(incidents_list) > (fetch_limit or 0)}"
    )

    return incidents_list_limit, final_created_after


def fetch_events_command(client, first_fetch, last_run, fetch_limit, fetch_delta, incidents_states):
    """
    Fetches incidents from the ProofPoint API.
    """
    last_fetch = last_run.get("last_fetch", {})
    last_fetched_id = last_run.get("last_fetched_incident_id", {})

    for state in incidents_states:
        if not last_fetch.get(state):
            last_fetch[state] = first_fetch
        if not last_fetched_id.get(state):
            last_fetched_id[state] = "0"

    incidents = []
    for state in incidents_states:
        request_params = {
            "created_after": last_fetch[state],
            "last_fetched_id": last_fetched_id[state],
            "fetch_delta": fetch_delta,
            "state": state,
            "fetch_limit": fetch_limit,
        }

        incidents_list, final_created_after = get_incidents_batch_by_time_request(client, request_params)
        incidents.extend(incidents_list)

        if incidents_list:
            id = incidents_list[-1].get("id")
            last_fetch_time = incidents_list[-1]["created_at"]
            last_fetch[state] = (datetime.strptime(last_fetch_time, TIME_FORMAT) - timedelta(minutes=1)).isoformat().split(".")[
                0
            ] + "Z"
            last_fetched_id[state] = id
        else:
            # Even when no incidents were found, persist the progress made by the batch loop
            # so the next fetch starts from where we left off (e.g., after hitting MAX_API_REQUESTS).
            last_fetch[state] = final_created_after

    demisto.debug(f"End of current fetch function with last_fetch {last_fetch!s} and last_fetched_id {last_fetched_id!s}")

    last_run = {"last_fetch": last_fetch, "last_fetched_incident_id": last_fetched_id}

    demisto.debug(f"Fetched {len(incidents)} events")

    events = get_events_from_incidents(incidents)
    return events, last_run


def get_events_from_incidents(incidents):
    """
    Parses events from incidents.
    Each incident contains list of events:
    {'id':1,
    'updated_at': '01-01-2020',
    'events': [first_event_data, second_event_data, ...]
    'additional_fields': ...
    }

    The function parses the data in the following way:
    {'id':1,
    'updated_at': '01-01-2020',
    'event': first_event_data
    'additional_fields': ...
    },
    {'id':1,
    'updated_at': '01-01-2020',
    'events': second_event_data
    'additional_fields': ...
    }
    ....

    :param incidents: list of incidents that contains events.
    :return: parsed events.
    """
    fetched_events = []
    for incident in incidents:
        if events := incident.get("events"):
            for event in events:
                new_incident = copy.deepcopy(incident)
                del new_incident["events"]
                new_incident["event"] = event
                fetched_events.append(new_incident)
        else:
            del incident["events"]
            incident["event"] = {}
            fetched_events.append(incident)
    return fetched_events


def main():  # pragma: no cover
    """main function, parses params and runs command functions"""
    args = demisto.args()
    command = demisto.command()
    params = demisto.params()

    api_key = params.get("credentials", {}).get("password")
    base_url = params.get("url")
    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)

    # How many time before the first fetch to retrieve incidents
    first_fetch, _ = parse_date_range(params.get("first_fetch", "3 days") or "3 days", date_format=TIME_FORMAT)
    fetch_limit = params.get("fetch_limit", "100")
    fetch_delta = params.get("fetch_delta", "6 hours")
    incidents_states = argToList(params.get("states", ["new", "open", "assigned", "closed", "ignored"]))

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

    try:
        headers = {"Content-Type": "application/json", "Authorization": api_key}
        client = Client(
            base_url=base_url,
            verify=verify_certificate,
            headers=headers,
            proxy=proxy,
        )

        if command == "test-module":
            return_results(test_module(client, first_fetch))

        elif command == "proofpoint-trap-get-events":
            should_push_events = args.pop("should_push_events")
            events, human_readable, raw_response = list_incidents_command(client, args)
            results = CommandResults(raw_response=raw_response, readable_output=human_readable)
            return_results(results)
            if argToBoolean(should_push_events):
                send_events_to_xsiam(events, VENDOR, PRODUCT)

        elif command == "fetch-events":
            last_run = demisto.getLastRun()
            demisto.debug(f"last_run before fetch_events_command {last_run=}")
            events, last_run = fetch_events_command(
                client,
                first_fetch,
                last_run,
                fetch_limit,
                fetch_delta,
                incidents_states,
            )

            send_events_to_xsiam(events, VENDOR, PRODUCT)
            demisto.debug(f'Fetched event ids: {[event.get("id") for event in events]}')
            demisto.debug(f"last_run after fetch_events_command {last_run=}")
            demisto.setLastRun(last_run)

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


if __name__ == "__builtin__" or __name__ == "builtins":
    main()