NetskopeEventCollector Deprecated

Deprecated. Use Netskope Event Collector v2 instead.

Analytics & SIEM · Netskope

Details

IDNetskopeEventCollector
ProviderNetskope
CategoryAnalytics & SIEM
From Version6.8.0
Docker Imagedemisto/python3:3.12.11.4508456
Supported ModulesAgentix XSIAM

README

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

Configure Netskope Event Collector in Cortex

Parameter Description Required
Server URL   True
API token   True
Trust any certificate (not secure)   False
Use system proxy settings   False
Max events per fetch The maximum amount of events to retrieve per each event type. For more information about event types see the help section. False

Fetch Events Limitation

The collector can handle up to 35K events per minute on average.

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.

netskope-get-events


Returns events extracted from SaaS traffic and or logs.

Base Command

netskope-get-events

Input

Argument Name Description Required
limit The maximum number of alerts to return (default: 10, maximum value - 10000). Optional
should_push_events Set this argument to True in order to create events, otherwise the command will only display them. Optional

Context Output

There is no context output for this command.

Command example

!netskope-get-events limit=1

Context Example

{
    "Netskope": {
        "Event": [
            {
                "_category_id": "8",
                "_correlation_id": "c66ef426-b403-4be5-8052-05d2c81ed321",
                "_ef_received_at": 1658102836562,
                "_event_id": "bd1074e2-fcbc-4c02-98f1-357aeb57f6c8",
                "_forwarded_by": "service-event-forwarder",
                "_gef_src_dp": "NL-AAA",
                "_id": "23a372c433381a6a11798123",
                "_insertion_epoch_timestamp": 1658102843,
                "_raw_event_inserted_at": 1658102836720,
                "_service_identifier": "service-test",
                "access_method": "API Connector",
                "acked": "false",
                "action": "anomaly_detection",
                "activity": "Login Successful",
                "alert": "yes",
                "alert_id": "62d4a3c35b8bdd69ad5e1234",
                "alert_name": "Alert Name",
                "alert_type": "test",
                "anomalyData": {
                    "_t": "CategoricalModeling",
                    "binCount": 6,
                    "convergenceFactor": 0.9863013699,
                    "featureValue": "1.1.1.1",
                    "histo": [
                        {
                            "bin": "2.2.2.2",
                            "count": 205
                        },
                        {
                            "bin": "3.3.3.3",
                            "count": 30
                        },
                        {
                            "bin": "4.4.4.4",
                            "count": 1
                        }
                    ],
                    "modelId": "test",
                    "observationCount": 0,
                    "percentileThresholdCount": 6,
                    "probability": 0,
                    "sampleCount": 438,
                    "scope": "User"
                },
                "anomaly_type": "test-type",
                "app": "Microsoft Office 365 Sharepoint Online",
                "appcategory": "Collaboration",
                "category": "Collaboration",
                "cci": 91,
                "ccl": "excellent",
                "count": 1,
                "createdTime": "2022-07-18 00:05:23.321000",
                "event_type": "alert",
                "instance_id": "test-instance",
                "organization_unit": "test",
                "other_categories": [],
                "score": 75,
                "severity": "Low",
                "site": "Microsoft Office 365 Sharepoint Sites",
                "src_country": "PH",
                "src_geoip_src": 2,
                "src_latitude": 456.789,
                "src_location": "Test",
                "src_longitude": 123.456,
                "src_region": "Province of Somewhere",
                "src_zipcode": "1234",
                "srcip": "6.6.6.6",
                "timestamp": "2022-07-17T23:48:52.000Z",
                "traffic_type": "CloudApp",
                "type": "nspolicy",
                "ur_normalized": "test@test.com",
                "user": "test@test.com",
                "userkey": "test@test.com",
                "windowId": 1658016000000
            },
            {
                "_category_id": "8",
                "_correlation_id": "57e53633-3eb9-4055-9e84-07de4c367347",
                "_ef_received_at": 1656449549192,
                "_event_id": "7dc94895-fe14-456d-b9c8-0a7f0dac5064",
                "_forwarded_by": "service-event-forwarder",
                "_gef_src_dp": "ABCD",
                "_id": "9f806593aa4385e4fc14865c",
                "_insertion_epoch_timestamp": 1656449557,
                "_raw_event_inserted_at": 1656449549850,
                "_service_identifier": "service-introspection",
                "_session_begin": 1,
                "access_method": "API Connector",
                "activity": "Login Successful",
                "alert": "no",
                "app": "Microsoft Office 365 Sharepoint Online",
                "app_activity": "UserLoggedIn",
                "app_session_id": 6162799428773683,
                "appcategory": "Collaboration",
                "browser": "unknown",
                "category": "Collaboration",
                "cci": 91,
                "ccl": "excellent",
                "count": 1,
                "device": "Other",
                "dst_latitude": "",
                "dst_longitude": "",
                "event_type": "application",
                "from_user": "test@test.com",
                "instance_id": "some-instance",
                "netskope_activity": "False",
                "object": "test@test.com",
                "object_id": "test@test.com",
                "object_type": "User",
                "organization_unit": "test",
                "os": "unknown",
                "other_categories": [],
                "site": "Microsoft Office 365 Sharepoint Sites",
                "src_country": "PH",
                "src_geoip_src": 2,
                "src_latitude": 456,
                "src_location": "test",
                "src_longitude": 123,
                "src_region": "Province of Test",
                "src_zipcode": "1234",
                "srcip": "2.2.2.2",
                "timestamp": "2022-06-28T16:59:15.000Z",
                "traffic_type": "CloudApp",
                "type": "nspolicy",
                "ur_normalized": "test@test.com",
                "user": "test@test.com",
                "userip": "2.2.2.2",
                "userkey": "test@test.com"
            },
            {
                "_id": "efac69202c964c91fd59bcb9",
                "_insertion_epoch_timestamp": 1658331170,
                "audit_log_event": "Client Disable Request Submitted",
                "ccl": "unknown",
                "count": 1,
                "event_type": "audit",
                "organization_unit": "test",
                "severity_level": 1,
                "supporting_data": {
                    "data_type": "hostname",
                    "data_values": "HAMRGBCNX147"
                },
                "timestamp": "2022-07-20T15:27:50.000Z",
                "type": "admin_audit_logs",
                "ur_normalized": "test@test.com",
                "user": "test@test.com"
            },
            {
                "_correlation_id": "5f3e3987-115c-4fed-9c5e-f69e184069af",
                "_ef_received_at": 1657742097188,
                "_event_id": "bd3de3e3-378e-4e01-ba8d-a5d72565bde7",
                "_forwarded_by": "msg-relayer",
                "_gef_src_dp": "IN-AAA1",
                "_id": "e03cf756afc2a707666fcbc0",
                "_insertion_epoch_timestamp": 1657742104,
                "_raw_event_inserted_at": 1657742097698,
                "_service_identifier": "service-npa",
                "_tenant_id": "test-tenant",
                "access_method": "Client",
                "action": "allow",
                "app": "[CS SEG's]",
                "appcategory": "n/a",
                "category": "",
                "cci": 0,
                "ccl": "unknown",
                "client_bytes": 1593,
                "client_packets": 13,
                "count": 1,
                "device": "Windows",
                "dsthost": "8.8.8.8",
                "dstip": "",
                "dstport": 443,
                "end_time": "2022-07-13T19:53:02+00:00",
                "event_type": "network",
                "hostname": "L-101861180",
                "ip_protocol": "TCP",
                "netskope_pop": "IN-AAA1",
                "network_session_id": "12345678",
                "num_sessions": 1,
                "numbytes": 2387,
                "organization_unit": "test",
                "os": "Windows",
                "os_version": "10.0 (2009)",
                "policy": "Netskope Private Apps Allowed",
                "protocol": "Http",
                "protocol_port": "TCP:443",
                "publisher_cn": "abcd1234",
                "publisher_name": "test",
                "server_bytes": 794,
                "server_packets": 11,
                "session_duration": 23461,
                "site": "1.1.1.1",
                "srcip": "",
                "srcport": 447,
                "start_time": "2022-07-13T19:52:51+00:00",
                "timestamp": "2022-07-13T19:54:57.000Z",
                "total_packets": 24,
                "traffic_type": "PrivateApp",
                "tunnel_id": "1150",
                "tunnel_type": "NPA",
                "tunnel_up_time": 23461,
                "type": "network",
                "ur_normalized": "test@test.com",
                "user": "test@test.com",
                "userip": "",
                "userkey": "test@test.com"
            }
        ]
    }
}

Human Readable Output

Events List

Id Timestamp Type Access Method App Traffic Type
23a372c433381a6a11798123 2022-07-17T23:48:52.000Z nspolicy API Connector Microsoft Office 365 Sharepoint Online CloudApp
9f806593aa4385e4fc14865c 2022-06-28T16:59:15.000Z nspolicy API Connector Microsoft Office 365 Sharepoint Online CloudApp
efac69202c964c91fd59bcb9 2022-07-20T15:27:50.000Z admin_audit_logs      
e03cf756afc2a707666fcbc0 2022-07-13T19:54:57.000Z network Client [CS SEG’s] PrivateApp

Configuration parameters

  • url — Server URL (required)
  • credentials — (required)
  • max_fetch — Max events per fetch
  • eventFetchInterval — Events Fetch Interval
  • event_types_to_fetch — Event Types To Fetch
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • netskope-get-events Deprecated

    Returns events extracted from SaaS traffic and or logs.

from typing import Any

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

from CommonServerUserPython import *  # noqa

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

""" CONSTANTS """

ALL_SUPPORTED_EVENT_TYPES = ["application", "alert", "page", "audit", "network", "incident"]
MAX_EVENTS_PAGE_SIZE = 10000
MAX_SKIP = 50000

EXECUTION_TIMEOUT_SECONDS = 190  # 3:30 minutes

# Netskope response constants
WAIT_TIME = "wait_time"  # Wait time between queries
RATE_LIMIT_REMAINING = "ratelimit-remaining"  # Rate limit remaining
RATE_LIMIT_RESET = "ratelimit-reset"  # Rate limit RESET value is in seconds

""" CLIENT CLASS """


class Client(BaseClient):
    """
    Client for Netskope RESTful API.

    Args:
        base_url (str): The base URL of Netskope.
        token (str): The token to authenticate against Netskope API.
        validate_certificate (bool): Specifies whether to verify the SSL certificate or not.
        proxy (bool): Specifies if to use XSOAR proxy settings.
    """

    def __init__(self, base_url: str, token: str, validate_certificate: bool, proxy: bool, event_types_to_fetch: list[str]):
        self.fetch_status: dict = {event_type: False for event_type in event_types_to_fetch}
        self.event_types_to_fetch: list[str] = event_types_to_fetch

        headers = {"Netskope-Api-Token": token}
        super().__init__(base_url, verify=validate_certificate, proxy=proxy, headers=headers)

    def perform_data_export(self, endpoint: str, _type: str, index_name: str, operation: str):
        url_suffix = f"events/dataexport/{endpoint}/{_type}"
        params = {"index": index_name, "operation": operation}
        response = self._http_request(method="GET", url_suffix=url_suffix, params=params, resp_type="response", retries=10)
        honor_rate_limiting(headers=response.headers, endpoint=url_suffix)
        return response.json()


""" HELPER FUNCTIONS """


def honor_rate_limiting(headers, endpoint):
    """
    Identify the response headers carrying the rate limiting value.
    If the rate limit remaining for this endpoint is 0 then wait for the rate limit reset time before sending the response to the
    client.
    """
    try:
        if RATE_LIMIT_REMAINING in headers:
            remaining = headers.get(RATE_LIMIT_REMAINING)
            demisto.debug(f"Remaining rate limit is: {remaining}")
            if int(remaining) <= 0:
                demisto.debug(f"Rate limiting reached for the endpoint: {endpoint}")
                if to_sleep := headers.get(RATE_LIMIT_RESET):
                    demisto.debug(f"Going to sleep for {to_sleep} seconds to avoid rate limit error")
                    time.sleep(int(to_sleep))
                else:
                    # if the RESET value does not exist in the header then
                    # sleep for default 1 second as the rate limit remaining is 0
                    demisto.debug("Did not find a rate limit reset value, going to sleep for 1 second to avoid rate limit error")
                    time.sleep(1)

    except ValueError as ve:
        logging.error(f"Value error when honoring the rate limiting wait time {headers} {ve!s}")


def populate_parsing_rule_fields(event: dict, event_type: str):
    """
    Handles the source_log_event and _time fields.
    Sets the source_log_event to the given event type and _time to the time taken from the timestamp field

    Args:
        event (dict): the event to edit
        event_type (str): the event type tp set in the source_log_event field
    """
    event["source_log_event"] = event_type
    try:
        event["_time"] = timestamp_to_datestring(event["timestamp"] * 1000, is_utc=True)
    except TypeError:
        # modeling rule will default on ingestion time if _time is missing
        pass


def prepare_events(events: list, event_type: str) -> list:
    """
    Iterates over a list of given events and add/modify special fields like event_id, _time and source_log_event.

    Args:
        events (list): list of events to modify.
        event_type (str): the type of events given in the list.

    Returns:
        list: the list of modified events
    """
    for event in events:
        populate_parsing_rule_fields(event, event_type)
        event_id = event.get("_id")
        event["event_id"] = event_id

    return events


def print_event_statistics_logs(events: list, event_type: str):
    """
    Helper function for debugging purposes.
    This function is responsible to print statistics about pulled events, like the amount of pulled events and the first event and
    last event times.

    Args:
        events (list): list of events.
        event_type (str): the type of events given in the list.
    """
    demisto.debug(f"__[{event_type}]__ - Total events fetched this round: {len(events)}")
    if events:
        event_times = (
            f'__[{event_type}]__ - First event: {events[0].get("timestamp")} __[{event_type}]__ - Last event: '
            f'{events[-1].get("timestamp")}'
        )
        demisto.debug(event_times)


def is_execution_time_exceeded(start_time: datetime) -> bool:
    """
    Checks if the execution time so far exceeded the timeout limit.

    Args:
        start_time (datetime): the time when the execution started.

    Returns:
        bool: true, if execution passed timeout settings, false otherwise.
    """
    end_time = datetime.utcnow()
    secs_from_beginning = (end_time - start_time).seconds
    demisto.debug(f"Execution length so far is {secs_from_beginning} secs")

    return secs_from_beginning > EXECUTION_TIMEOUT_SECONDS


def remove_unsupported_event_types(last_run_dict: dict, event_types_to_fetch: list):
    keys_to_remove = []

    for key in last_run_dict:
        if (key in ALL_SUPPORTED_EVENT_TYPES) and (key not in event_types_to_fetch):
            keys_to_remove.append(key)

    for key in keys_to_remove:
        last_run_dict.pop(key, None)


def setup_last_run(last_run_dict: dict, event_types_to_fetch: list[str]) -> dict:
    """
    Setting the last_tun object with the right operation to be used throughout the integration run.

    Args:
        last_run_dict (dict): The dictionary of the last run to be configured

    Returns:
        dict: the modified last run dictionary with the needed operation
    """
    remove_unsupported_event_types(last_run_dict, event_types_to_fetch)
    first_fetch = int(arg_to_datetime("now").timestamp())  # type: ignore[union-attr]
    for event_type in event_types_to_fetch:
        if not last_run_dict.get(event_type, {}).get("operation"):
            last_run_dict[event_type] = {"operation": first_fetch}

    demisto.debug(f"Initialize last run to - {last_run_dict}")

    return last_run_dict


def handle_data_export_single_event_type(
    client: Client, event_type: str, operation: str, limit: int, execution_start_time: datetime, all_event_types: list
) -> bool:
    """
    Pulls events per each given event type. Each event type receives a dedicated index name that is constructed using the event
    type and the integration instance name. The function keeps pulling events as long as the limit was not exceeded.
    - First thing it validates is that execution time of the entire run was not exceeded.
    - Then it checks if we need to wait some time before making another call to the same endpoint by checking the wait_time value
        received in the previous response.
    - The operation variable marks the next operation to perform on this endpoint (besides the first fetch it is always 'next')
    - After it is done pulling, it marks this event type as successfully done in the 'fetch_status' dictionary.

    Args:
        client (Client): The Netskope client.
        event_type (str): The type of event to pull.
        operation (str): The operation to perform. Can be 'next' or a timestamp string.
        limit (int): The limit which after we stop pulling.
        execution_start_time (datetime): The time when we started running the fetch mechanism.

    Return:
        list: The list of events pulled for the given event type.
        bool: Was execution timeout reached.
    """
    wait_time: int = 0
    events: list[dict] = []
    # We use the instance name to allow multiple instances in parallel without causing a collision in index names
    instance_name = demisto.callingContext.get("context", {}).get("IntegrationInstance")
    index_name = f"xsoar_collector_{instance_name}_{event_type}"

    while len(events) < limit:
        # If the execution exceeded the timeout we will break
        if is_execution_time_exceeded(start_time=execution_start_time):
            return True

        # Wait time between queries
        if wait_time:
            demisto.debug(f"Going to sleep between queries, wait_time is {wait_time} seconds")
            time.sleep(wait_time)  # pylint: disable=E9003
        else:
            demisto.debug("No wait time received, going to sleep for 1 second")
            time.sleep(1)

        response = client.perform_data_export("events", event_type, index_name, operation)

        results = response.get("result", [])
        demisto.debug(f"The number of received events - {len(results)}")
        operation = "next"

        # The API responds with the time we should wait between requests, the server needs this time to prepare the next response.
        # It will be used to sleep in the beginning of the next iteration
        wait_time = arg_to_number(response.get(WAIT_TIME, 5)) or 5
        demisto.debug(f"Wait time is {wait_time} seconds")

        events.extend(results)

        all_event_types.extend(prepare_events(results, event_type))

        if not results or len(results) < MAX_EVENTS_PAGE_SIZE:
            break

    print_event_statistics_logs(events=events, event_type=event_type)
    # We mark this event type as successfully fetched
    client.fetch_status[event_type] = True
    return False


def get_all_events(client: Client, last_run: dict, all_event_types: list, limit: int = MAX_EVENTS_PAGE_SIZE) -> dict:
    """
    Iterates over all supported event types and call the handle data export logic. Once each event type is done the operation for
    next run is set to 'next'.

    Args:
        client (Client): The Netskope client.
        last_run (dict): The execution last run dict where the relevant operations are stored.
        limit (int): The limit which after we stop pulling.

    Returns:
        list: The accumulated list of all events.
        dict: The updated last_run object.
    """

    execution_start_time = datetime.utcnow()
    for event_type in client.event_types_to_fetch:
        event_type_operation = last_run.get(event_type, {}).get("operation")

        time_out = handle_data_export_single_event_type(
            client=client,
            event_type=event_type,
            operation=event_type_operation,
            limit=limit,
            execution_start_time=execution_start_time,
            all_event_types=all_event_types,
        )
        last_run[event_type] = {"operation": "next"}

        if time_out:
            demisto.info("Timeout reached, stopped pulling events")
            break

    return last_run


""" COMMAND FUNCTIONS """


def test_module(client: Client, last_run: dict, max_fetch: int) -> str:
    get_all_events(client, last_run, limit=max_fetch, all_event_types=[])
    return "ok"


def get_events_command(client: Client, args: dict[str, Any], last_run: dict, events: list) -> tuple[CommandResults, list]:
    limit = arg_to_number(args.get("limit")) or 10
    _ = get_all_events(client=client, last_run=last_run, limit=limit, all_event_types=events)

    for event in events:
        event["timestamp"] = timestamp_to_datestring(event["timestamp"] * 1000)

    readable_output = tableToMarkdown(
        "Events List:",
        events,
        removeNull=True,
        headers=["_id", "timestamp", "type", "access_method", "app", "traffic_type"],
        headerTransform=string_to_table_header,
    )

    results = CommandResults(
        outputs_prefix="Netskope.Event",
        outputs_key_field="_id",
        outputs=events,
        readable_output=readable_output,
        raw_response=events,
    )

    return results, events


def handle_event_types_to_fetch(event_types_to_fetch) -> list[str]:
    """Handle event_types_to_fetch parameter.
    Transform the event_types_to_fetch parameter into a pythonic list with lowercase values.
    """
    return argToList(
        arg=event_types_to_fetch if event_types_to_fetch else ALL_SUPPORTED_EVENT_TYPES,
        transform=lambda x: x.lower(),
    )


def next_trigger_time(num_of_events, max_fetch, new_last_run):
    """Check wether to add the next trigger key to the next_run dict based on number of fetched events.

    Args:
        num_of_events (int): The number of events fetched.
        max_fetch (int): The maximum fetch limit.
        new_last_run (dict): the next_run to update
    """
    if num_of_events > (max_fetch / 2):
        new_last_run["nextTrigger"] = "0"
    else:
        new_last_run.pop("nextTrigger", None)


""" MAIN FUNCTION """


def main() -> None:  # pragma: no cover
    try:
        params = demisto.params()

        url = params.get("url")
        token = params.get("credentials", {}).get("password")
        base_url = urljoin(url, "/api/v2/")
        verify_certificate = not params.get("insecure", False)
        proxy = params.get("proxy", False)
        max_fetch: int = arg_to_number(params.get("max_fetch")) or 10000
        vendor, product = params.get("vendor", "netskope"), params.get("product", "netskope")
        event_types_to_fetch = handle_event_types_to_fetch(params.get("event_types_to_fetch"))
        demisto.debug(f"Event types that will be fetched in this instance: {event_types_to_fetch}")
        command_name = demisto.command()
        demisto.debug(f"Command being called is {command_name}")

        client = Client(base_url, token, verify_certificate, proxy, event_types_to_fetch)
        last_run = setup_last_run(demisto.getLastRun(), event_types_to_fetch)
        demisto.debug(f"Running with the following last_run - {last_run}")

        all_event_types: list[dict] = []
        new_last_run: dict = {}
        if command_name == "test-module":
            # This is the call made when pressing the integration Test button.
            result = test_module(client, last_run, max_fetch=MAX_EVENTS_PAGE_SIZE)  # type: ignore[arg-type]
            return_results(result)

        elif command_name == "netskope-get-events":
            results, events = get_events_command(client, demisto.args(), last_run, events=[])
            if argToBoolean(demisto.args().get("should_push_events", "true")):
                send_events_to_xsiam(events=events, vendor=vendor, product=product, chunk_size=XSIAM_EVENT_CHUNK_SIZE_LIMIT)  # type: ignore
            return_results(results)

        elif command_name == "fetch-events":
            # We have this try-finally block for fetch events where wrapping up should be done if errors occur
            start = datetime.utcnow()
            try:
                demisto.debug(f"Sending request with last run {last_run}")
                new_last_run = get_all_events(client=client, last_run=last_run, limit=max_fetch, all_event_types=all_event_types)
            finally:
                demisto.debug(f"sending {len(all_event_types)} to xsiam")
                send_events_to_xsiam(
                    events=all_event_types, vendor=vendor, product=product, chunk_size=XSIAM_EVENT_CHUNK_SIZE_LIMIT
                )

                for (
                    event_type,
                    status,
                ) in client.fetch_status.items():
                    if not status:
                        new_last_run[event_type] = {"operation": "resend"}

                end = datetime.utcnow()

                demisto.debug(f"Handled {len(all_event_types)} total events in {(end - start).seconds} seconds")
                next_trigger_time(len(all_event_types), max_fetch, new_last_run)
                demisto.debug(f"Setting the last_run to: {new_last_run}")
                demisto.setLastRun(new_last_run)

    # Log exceptions and return errors
    except Exception as e:
        last_run = new_last_run if new_last_run else demisto.getLastRun()
        last_run.pop("nextTrigger", None)
        demisto.setLastRun(last_run)
        demisto.debug(f"last run after removing nextTrigger {last_run}")
        return_error(f"Failed to execute {command_name} command.\nError:\n{e!s}")


""" ENTRY POINT """

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