KnowBe4 KMSAT Event Collector

KnowBe4_KMSAT allows you to push and pull your external data to and from the KnowBe4 console.

Analytics & SIEM · KMSAT

Details

IDKnowBe4 KMSAT Event Collector
ProviderVista Equity Partners
CategoryAnalytics & SIEM
From Version6.8.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix XSIAM

README

Allows you to push and pull your external data to and from the KnowBe4 console.

Configure KnowBe4 KMSAT Event Collector in Cortex

Parameter Description Required
Your server URL   True
API Key The API Key to use for connection. For more information about how to generate an API Key, refer to https://support.knowbe4.com/hc/en-us/articles/360024863474-User-Event-API True
First fetch time interval The time range to consider for the initial data fetch. (<number> <unit>, e.g., 2 days, 2 months, 2 years). Default is 1 day. False
Events Fetch Interval The Fetch interval. It is recommended to set it to 5 hours as there are not many events for this API and there’s an api-calls daily-limit for the basic API key. False
Trust any certificate (not secure)   False
Use system proxy settings   False

Important Notes
The basic API-Key has a daily limit of calls per seat.
Therefore, the default and recommended Events Fetch Interval value is 5 hours and
First fetch time interval is 1 day.

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.

kms-get-events

Manual command to fetch events and display them.

Base Command

kms-get-events

Input

Argument Name Description Required
occurred_date Filter by the date the event occurred (YYYY-MM-DD). Optional
risk_level Filter by the risk level by entering a value from -10 (low risk) to 10 (high risk). Optional
per_page The number of results to display per page. The maximum and default is 100. Optional
page The results page to display. Optional
should_push_events Set this argument to True in order to create events, otherwise the command will only display them. If setting to ‘False’, the returned events will be lost. Possible values are: True, False. Default is False. Required

Context Output

Path Type Description
KMSat.Event.id Number Event ID.
KMSat.Event.user.email String The target mail for this event.
KMSat.Event.user.id Number The ID of the user the event is targeted to.
KMSat.Event.user.archived Boolean Whether the user is archived or not.
KMSat.Event.external_id String The event’s external ID.
KMSat.Event.source String The source of the event.
KMSat.Event.description String The event description.
KMSat.Event.occurred_date String The date the event occurred.
KMSat.Event.risk.level Number The event’s risk level.
KMSat.Event.risk.factor Number The event’s risk factor.
KMSat.Event.risk.decay_mode String The risk’s decay mode.
KMSat.Event.risk.expire_date String The event’s expiration date.
KMSat.Event.event_type.id Number The ID of the event type.
KMSat.Event.event_type.name String The name of the event type.

Command example

!kms-get-events should_push_events=false

Context Example

{
    "KMSat": {
        "Event": [
            {
                "account_id": 52306,
                "description": "My description",
                "event_type": {
                    "description": null,
                    "id": 418927900,
                    "name": "my_custom_event"
                },
                "external_id": null,
                "id": "2b265035-1a12-4e76-bcb1-6c681b86333e",
                "metadata": null,
                "occurred_date": "2022-08-04T14:14:50.917Z",
                "risk": {
                    "decay_mode": 0,
                    "expire_date": null,
                    "level": 5
                },
                "source": null,
                "user": {
                    "archived": false,
                    "email": "example@example.com",
                    "id": 38651943
                }
            }
        ]
    }
}

Human Readable Output

KnowBe4 KMSAT Logs

AccountId Description EventType Id OccurredDate Risk User
52306 My description lkjhy khl lgf id: 420899085
name: event_type_55
description: null
786a515c-1cbd-4a8c-a94a-61ad877c893c 2022-08-09T10:05:13.890Z level: 5
decay_mode: 0
expire_date: null
email: maizen@example.com
id: 38651943
archived: false
52306 My description lkjhy khl lgf id: 420894024
name: event_type_2
description: null
c3081dfc-1bf9-4c56-b6ff-f364f0c13d39 2022-08-09T10:01:45.862Z level: 5
decay_mode: 0
expire_date: null
email: maizen@example.com
id: 38651943
archived: false
52306 My description id: 418927900
name: my_custom_event
description: null
2b265035-1a12-4e76-bcb1-6c681b86333e 2022-08-04T14:14:50.917Z level: 5
decay_mode: 0
expire_date: null
email: maizen@example.com
id: 38651943
archived: false

Configuration parameters

  • url — Your server URL (required)
  • credentials — (required)
  • first_fetch — First fetch time interval
  • eventFetchInterval — Events Fetch Interval
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • kms-get-events

    Manual command to fetch and display events.

from datetime import date

import demistomock as demisto  # noqa: F401
import urllib3
from CommonServerPython import *  # noqa: F401

from CommonServerUserPython import *  # noqa

# Disable insecure warnings
urllib3.disable_warnings()

""" CONSTANTS """

DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR
MAX_EVENTS_PER_REQUEST = 100
VENDOR = "knowbe4"
PRODUCT = "kmsat"

""" CLIENT CLASS """


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

    This Client implements API calls to the KnowBe4 KMSAT platform, and does not contain any XSOAR logic.

    :param base_url (str): Saas Security server url.
    :param verify (bool): specifies whether to verify the SSL certificate or not.
    :param proxy (bool): specifies if to use XSOAR proxy settings.
    :param headers (dict[str, str]): the requests header.
    """

    @logger
    def __init__(self, base_url: str, verify: bool = False, proxy: bool = False, headers: dict[str, str] = {}):
        super().__init__(base_url, verify=verify, proxy=proxy)
        self.headers = headers

    def http_request(
        self,
        method: str = "GET",
        params: dict = None,
        url_suffix: str = "",
        resp_type: str = "response",
        ok_codes: list[int] = [200],
    ):
        """
        Overrides Base client request function.

        :return: The http response
        """
        # token = self.get_access_token()
        return super()._http_request(
            headers=self.headers, method=method, params=params, url_suffix=url_suffix, resp_type=resp_type, ok_codes=ok_codes
        )  # type: ignore[misc]

    def get_events_request(self, params: dict = None):  # pragma: no cover
        try:
            return self.http_request(method="GET", url_suffix="/events", resp_type="response", ok_codes=[200, 204], params=params)
        except Exception as e:
            if "Limit Exceeded" in str(e):
                raise DemistoException(
                    "You've reached the daily api-call limit for your key.\n"
                    "Please wait for tomorrow to reset your calls limit or upgrade your key."
                )
            else:
                raise DemistoException(str(e))


""" HELPER FUNCTIONS """


def eliminate_duplicated_events(fetched_events: list[dict], last_run: dict[str, date]):
    """
    create a new list out of a given a list that include only events that occurred after that latest event from previous run.

    Args:
        fetched_events (list[dict]): the list of the fetched events.
        last_run (dict[str, date]): the occurred time of the lastest event for previous run.

    Returns:
        list: A list containing only events that occurred after the last run.
    """
    last_run_time = last_run.get("latest_event_time")
    return [event for event in fetched_events if parse_date_string(event.get("occurred_date")) > last_run_time]


def check_if_last_run_reached(last_run: dict[str, date], earliest_fetched_event: dict[str, Any]):
    """
    Compare the latest event from previous fetch interval with the latest event in the page from the current fetch interval
    To check if the latest event was reached.

    Args:
        earliest_fetched_event (dict): the earliest event from the current fetch interval.
        last_run (dict[str, date]): the occurred time of the lastest event for previous run.

    Returns:
        list: A list containing only events that occurred after the last run.
    """
    last_run_time = last_run.get("latest_event_time")
    return last_run_time >= parse_date_string(earliest_fetched_event.get("occurred_date"))


""" COMMAND FUNCTIONS """


def fetch_events(
    client: Client, first_fetch_time: Optional[datetime] = datetime.now(), last_run: dict[str, date] = {}
) -> tuple[List[dict], dict[str, date]]:
    """
    Fetches events from the KnowBe4_KMSAT queue.
    """
    query_params = {"page": 1, "per_page": 100}
    events: List[dict] = []
    if not last_run and first_fetch_time:
        last_run["latest_event_time"] = first_fetch_time
    elif type(last_run.get("latest_event_time")) is str:
        last_run["latest_event_time"] = parse_date_string(last_run.get("latest_event_time"))
    while True:
        response = client.get_events_request(params=query_params).json()
        fetched_events = response.get("data") or []
        if not fetched_events:
            demisto.debug("no events fetched from the api at all")
            break
        demisto.info(f"fetched events length: ({len(fetched_events)})")
        demisto.debug(f"fetched events: ({fetched_events})")
        is_last_run_reached = check_if_last_run_reached(last_run, fetched_events[-1])
        if not response.get("meta", {}).get("next_page") or is_last_run_reached:
            events.extend(eliminate_duplicated_events(fetched_events, last_run))
            break
        events.extend(fetched_events)
        query_params["page"] = response.get("meta", {}).get("next_page", 1)
    new_last_run_obj: dict = {
        "latest_event_time": events[0].get("occurred_date") if events else datetime.now(tz=timezone.utc).strftime(DATE_FORMAT)
    }
    demisto.info(f"Done fetching {len(events)} events, Setting new_last_run = {new_last_run_obj}.")
    return events, new_last_run_obj


def test_module(client: Client) -> str:
    """
    Testing we have a valid connection to Saas-Security.
    """
    try:
        client.get_events_request()
        return "ok"
    except Exception as e:
        if "Limit Exceeded" in str(e):
            raise DemistoException(
                "You've reached the daily api-call limit for your key.\n"
                "Please wait for tomorrow to reset your calls limit or upgrade your key."
            )
        elif "Internal Server Error" in str(e):
            raise DemistoException("Please make sure you've entered a valid api-key and chose the right server url.")
        else:
            raise DemistoException(str(e))


def get_events_command(client: Client, args: dict, vendor: str, product: str) -> Union[str, CommandResults]:
    """
    Fetches events from the KnowBe4-KMSAT queue and return them to the war-room.
    in case should_push_events is set to True, they will be also sent to XSIAM.
    """
    should_push_events = argToBoolean(args.get("should_push_events"))
    params = {"per_page": 100}
    args.pop("should_push_events")
    params.update(args)
    response = client.get_events_request(params)
    events: List[dict] = response.json().get("data") or []
    if events:
        if should_push_events:
            send_events_to_xsiam(events=events, vendor=vendor, product=product)
        return CommandResults(
            readable_output=tableToMarkdown(
                "KnowBe4 KMSAT Logs",
                events,
                # headers=['log_type', 'item_type', 'item_name', 'timestamp', 'serial'],
                headerTransform=underscoreToCamelCase,
                removeNull=True,
            ),
            raw_response=events,
            outputs=events,
            # outputs_key_field=['timestamp', 'log_type', 'item_name', 'item_type'],
            outputs_prefix="KMSat.Event",
        )
    return CommandResults(readable_output="No events were found.")


""" MAIN FUNCTION """


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

    base_url: str = params["url"].rstrip("/")
    api_key = params.get("credentials", {}).get("password")
    verify_certificate = not params.get("insecure", False)
    first_fetch_time = arg_to_datetime(params.get("first_fetch", "1 day"))
    proxy = demisto.params().get("proxy", False)
    vendor = VENDOR
    product = PRODUCT
    headers = {"Authorization": f"Bearer {api_key}"}

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

        if command == "test-module":
            return_results(test_module(client))
        elif command == "fetch-events":
            last_run = demisto.getLastRun()
            events, last_run = fetch_events(client=client, first_fetch_time=first_fetch_time, last_run=last_run)
            send_events_to_xsiam(events, vendor=vendor, product=product)
            demisto.setLastRun(last_run)
        elif command == "kms-get-events":
            return_results(get_events_command(client=client, args=args, vendor=vendor, product=product))
        else:
            raise ValueError(f"Command {command} is not implemented in this integration")
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{e!s}")


""" ENTRY POINT """


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