KeeperSecurity

Use this integration to fetch audit logs from Keeper Security Admin Console as XSIAM events.

Authentication & Identity Management · Keeper Security

Details

IDKeeperSecurity
ProviderKeeper Security Inc.
CategoryAuthentication & Identity Management
From Version6.8.0
Docker Imagedemisto/keepercommander:1.0.0.10182333
Supported ModulesXSIAM

README

Use this integration to fetch audit logs from Keeper Security Admin Console as XSIAM events.
This integration was integrated and tested with version 16.11.8 of Keeper Commander.

Configure Keeper Secrets Manager Event Collector in Cortex

Parameter Description Required
Server URL The server URL. For more help, checkout the ‘Server Regions’ section in the description. True
Username   True
Password   True
Maximum number of Alerts to fetch. The maximum number of Alert events to fetch.  
Trust any certificate (not secure)   False
Use system proxy settings   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.

keeper-security-register-start


Use this command to start the registration process.

Base Command

keeper-security-register-start

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Human Readable Output

Code was sent successfully to the user’s email

keeper-security-register-complete


Use this command to complete the registration process.

Base Command

keeper-security-register-complete

Input

Argument Name Description Required
code The authorization code retrieved from user’s email. Optional

Context Output

There is no context output for this command.

Human Readable Output

Login completed

keeper-security-register-test


Use this command to test the connectivity of the instance.

Base Command

keeper-security-register-test

Input

There is no context output for this command.

Context Output

There is no context output for this command.

Human Readable Output

Successful connection

Configuration parameters

  • url — Server URL (required)
  • credentials — Username (required)
  • alerts_max_fetch — Maximum number of Alerts to fetch.
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (3)

  • keeper-security-register-complete

    Use this command to complete the registration process.

  • keeper-security-register-start

    Use this command to start the registration process.

  • keeper-security-register-test

    Use this command to test the connectivity of the instance.

from datetime import datetime

import demistomock as demisto  # noqa: F401
import urllib3
from CommonServerPython import *  # noqa: F401
from keepercommander import api, crypto, utils
from keepercommander.auth.login_steps import DeviceApprovalChannel, LoginStepDeviceApproval, LoginStepPassword
from keepercommander.loginv3 import InvalidDeviceToken, LoginV3API, LoginV3Flow
from keepercommander.params import KeeperParams
from keepercommander.proto import APIRequest_pb2

""" CONSTANTS """

# We fetch from the Keeper Security Admin Console, so the Product is not "Security", but we assigned it as such
# so the dataset could have the name keeper_security_raw
VENDOR = "Keeper"
PRODUCT = "Security"
LOG_LINE = f"{VENDOR}_{PRODUCT}:"
DEFAULT_MAX_FETCH = 10000
API_MAX_FETCH = 1000
SESSION_TOKEN_TTL = 3600  # In seconds
REGISTRATION_FLOW_MESSAGE = (
    "In order to authorize the instance, first run the command `!keeper-security-register-start`."
    " A code will be sent to your email, copy it and paste that value in the command"
    " `!keeper-security-register-complete` as an argument to finish the process."
)
DEVICE_ALREADY_REGISTERED = (
    "Device is already registered, try running the 'keeper-security-register-complete'"
    " command without supplying a code argument."
)
SSO_REDIRECT = "Login was redirected to a cloud SSO. Please disable SSO redirect to continue."
LAST_RUN = "Last Run"
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"  # ISO8601 format with UTC, default in XSOAR

# Disable insecure warnings
urllib3.disable_warnings()


""" HELPER FUNCTIONS """


def get_current_time_in_seconds() -> int:
    """A function to return time as an int number of seconds since the epoch

    Returns:
        int: Number of seconds since the epoch
    """
    return int(datetime.now().timestamp())


def load_integration_context_into_keeper_params(
    username: str,
    password: str,
    server_url: str,
):
    """Load data from the integration context into a KeeperParams instance, which is used
    to communicate with the product. We call it inside the init method of the Client that
    will be used to communicate with the product.

    Args:
        username (str): The username of the account.
        password (str): The password of the account.
        server_url (str): The server URL.

    Returns:
        KeeperParams: An instance that will be used to communicate with the product.
    """
    integration_context = get_integration_context()
    keeper_params = KeeperParams()
    keeper_params.user = username
    keeper_params.password = password
    keeper_params.server = server_url

    # To allow requests to bypass proxy
    # ref -> https://docs.keeper.io/en/v/secrets-manager/commander-cli/troubleshooting-commander-cli#ssl-certificate-errors
    keeper_params.rest_context.certificate_check = False

    keeper_params.device_token = integration_context.get("device_token")
    keeper_params.device_private_key = integration_context.get("device_private_key")
    keeper_params.session_token = integration_context.get("session_token")
    keeper_params.clone_code = integration_context.get("clone_code")
    return keeper_params


def append_to_integration_context(context_to_append: dict[str, Any]):
    """Appends data to the integration context DB.

    Args:
        context_to_append (dict[str, Any]): Context data to append
    """
    integration_context = get_integration_context()
    integration_context |= context_to_append
    set_integration_context(integration_context)


""" CLIENT CLASS """


class Client:
    class DeviceApproval(LoginStepDeviceApproval):
        """
        In charge of sending and verifying the code sent to the user's email when registering the device for the first time.
        LoginStepDeviceApproval is is an abstract class that must be implemented. Some of the abstract methods
        are not needed, therefore, we implement them by including a pass segment.
        """

        @property
        def username(self):
            pass

        def cancel(self):
            pass

        def send_push(
            self,
            params: KeeperParams,
            channel: DeviceApprovalChannel,
            encryptedDeviceToken: bytes,
            encryptedLoginToken: bytes,
        ):
            LoginV3Flow.verifyDevice(
                params, encryptedDeviceToken, encryptedLoginToken, approval_action="push", approval_channel=channel
            )

        def send_code(
            self,
            params: KeeperParams,
            channel: DeviceApprovalChannel,
            encryptedDeviceToken: bytes,
            encryptedLoginToken: bytes,
            code: str,
        ):
            LoginV3Flow.verifyDevice(
                params,
                encryptedDeviceToken,
                encryptedLoginToken,
                approval_action="code",
                approval_channel=channel,
                approval_code=code,
            )

        def resume(self):
            pass

    class PasswordStep(LoginStepPassword):
        """
        In charge of verifying the user's password after verifying the device registration.
        LoginStepPassword is is an abstract class that must be implemented. Some of the abstract methods
        are not needed, therefore, we implement them by including a pass segment.
        """

        def __init__(self, salt_bytes: bytes, salt_iterations: int):
            self.salt_bytes = salt_bytes
            self.salt_iterations = salt_iterations

        @property
        def username(self):
            pass

        def forgot_password(self):
            pass

        def verify_password(self, params: KeeperParams, encryptedLoginToken: bytes) -> Any:
            # This function returns the data type APIRequest_pb2.LoginResponse
            params.auth_verifier = crypto.derive_keyhash_v1(params.password, self.salt_bytes, self.salt_iterations)
            return LoginV3API.validateAuthHashMessage(params, encryptedLoginToken)

        def verify_biometric_key(self, biometric_key: bytes):
            pass

        def cancel(self):
            pass

    def __init__(
        self,
        server_url: str,
        username: str,
        password: str,
    ) -> None:
        self.keeper_params: KeeperParams = load_integration_context_into_keeper_params(
            username=username,
            password=password,
            server_url=server_url,
        )

    def refresh_session_token_if_needed(
        self,
    ) -> None:
        """Refresh the session token if needed.

        Raises:
            DemistoException: If saved the TTL of the session token, but it is not found in the
            integration's context.
        """
        integration_context = get_integration_context()
        valid_until = integration_context.get("valid_until", 0)
        current_time = get_current_time_in_seconds()
        if self.keeper_params.session_token and current_time >= valid_until - 10:
            demisto.info("Refreshing session token")
            encrypted_device_token = LoginV3API.get_device_id(self.keeper_params)
            resp = self.save_device_tokens(
                encrypted_device_token=encrypted_device_token,
            )
            encrypted_login_token: bytes = resp.encryptedLoginToken

            self.validate_device_registration(
                encrypted_device_token=encrypted_device_token,
                encrypted_login_token=encrypted_login_token,
            )
            self.save_session_token()
        else:
            demisto.info("No need to refresh session token")

    def save_device_tokens(self, encrypted_device_token: bytes) -> Any:
        """Save the devices' tokens when starting to verify the device registration.

        Args:
            encrypted_device_token (bytes): The encrypted device token.

        Returns:
            APIRequest_pb2.LoginResponse: The response that holds data about the API call.
        """
        # This function returns the data type APIRequest_pb2.LoginResponse
        resp = LoginV3API.startLoginMessage(self.keeper_params, encrypted_device_token, cloneCode=None, loginType="NORMAL")
        append_to_integration_context(
            {
                "device_private_key": self.keeper_params.device_private_key,
                "device_token": self.keeper_params.device_token,
                "login_token": utils.base64_url_encode(resp.encryptedLoginToken),
            }
        )
        return resp

    def start_registering_device(
        self,
        device_approval: DeviceApproval,
        new_device: bool = False,
    ):
        """Start the registration process of a new or old device.

        Args:
            device_approval (DeviceApproval): DeviceApproval instance that is in charge of sending the code to the
            user's email.
            new_device (bool, optional): If we should configure a new device. Defaults to False.

        Raises:
            DemistoException: If we try registering an already registered and authenticated device.
            DemistoException: If we get a response status that we don't know how to handle.
        """
        encryptedDeviceToken = LoginV3API.get_device_id(self.keeper_params, new_device)
        resp = self.save_device_tokens(
            encrypted_device_token=encryptedDeviceToken,
        )
        if resp.loginState == APIRequest_pb2.DEVICE_APPROVAL_REQUIRED:  # pylint: disable=no-member
            # client goes to "standard device approval"
            device_approval.send_push(
                self.keeper_params,
                DeviceApprovalChannel.Email,
                encryptedDeviceToken,
                resp.encryptedLoginToken,
            )
        elif resp.loginState == APIRequest_pb2.REQUIRES_AUTH_HASH:  # pylint: disable=no-member
            raise DemistoException(DEVICE_ALREADY_REGISTERED)
        elif resp.loginState == APIRequest_pb2.REDIRECT_CLOUD_SSO:  # pylint: disable=no-member
            raise DemistoException(SSO_REDIRECT)
        else:
            raise DemistoException(f"Unknown login state {resp.loginState}")

    def validate_device_registration(
        self,
        encrypted_device_token: bytes,
        encrypted_login_token: bytes,
    ):
        """Verify the registration process of a new or old device. This method is also used as part of the
        mechanism to refresh the session token.

        Args:
            encrypted_device_token (bytes): The encrypted device token.
            encrypted_login_token (bytes): The encrypted login token.

        Raises:
            DemistoException: When trying to verify the user's password, and an error occurs.
            DemistoException: When trying to verify the device registration, and an error occurs.
        """
        resp = LoginV3API.startLoginMessage(self.keeper_params, encrypted_device_token)
        if resp.loginState == APIRequest_pb2.REQUIRES_AUTH_HASH:  # pylint: disable=no-member
            salt = api.get_correct_salt(resp.salt)
            password_step = self.PasswordStep(salt_bytes=salt.salt, salt_iterations=salt.iterations)
            verify_password_response = password_step.verify_password(self.keeper_params, encrypted_login_token)
            # Disabling pylint due to external class declaration
            if verify_password_response.loginState == APIRequest_pb2.LOGGED_IN:  # pylint: disable=no-member
                LoginV3Flow.post_login_processing(self.keeper_params, verify_password_response)
            else:
                raise DemistoException(
                    "Unknown login state after verify"
                    # Disabling pylint due to external class declaration
                    f" password {verify_password_response.loginState}"  # pylint: disable=no-member
                )
        else:
            raise DemistoException(f"Unknown login state {resp.loginState}")

    def finish_registering_device(
        self,
        device_approval: DeviceApproval,
        encrypted_login_token: bytes,
        code: str = "",
    ):
        """Finish the registration process of a new or old device. If the code argument is given, then
        we are verifying the newly registered device.

        Args:
            device_approval (DeviceApproval): DeviceApproval instance that is in charge of verifying the code sent
            to user's email.
            encrypted_login_token (bytes): The encrypted login token.
            code (str, optional): The code sent to the user's email. Defaults to "".
        """
        encrypted_device_token = utils.base64_url_decode(self.keeper_params.device_token)
        if code:
            device_approval.send_code(
                self.keeper_params,
                DeviceApprovalChannel.Email,
                encrypted_device_token,
                encrypted_login_token,
                code,
            )
        self.validate_device_registration(
            encrypted_device_token=encrypted_device_token,
            encrypted_login_token=encrypted_login_token,
        )

    def start_registration(self):
        device_approval = self.DeviceApproval()
        try:
            self.start_registering_device(device_approval)
        except InvalidDeviceToken:
            demisto.info("Registering new device")
            self.start_registering_device(device_approval, new_device=True)

    def save_session_token(
        self,
    ):
        append_to_integration_context(
            {
                "session_token": self.keeper_params.session_token,
                "clone_code": self.keeper_params.clone_code,
                "valid_until": get_current_time_in_seconds() + SESSION_TOKEN_TTL,
            }
        )

    def complete_registration(self, code: str):
        device_approval = self.DeviceApproval()
        integration_context = get_integration_context()
        encrypted_login_token = utils.base64_url_decode(integration_context["login_token"])
        self.finish_registering_device(device_approval, encrypted_login_token, code)
        self.save_session_token()
        if not self.keeper_params.session_token:
            raise DemistoException("Could not find session token")

    def query_audit_logs(self, limit: int, start_event_time: int) -> dict[str, Any]:
        request_query = {
            "command": "get_audit_event_reports",
            "report_type": "raw",
            "scope": "enterprise",
            "limit": limit,
            "order": "ascending",
            "filter": {
                "created": {"min": start_event_time},
            },
        }
        return api.communicate(self.keeper_params, request_query)

    def test_registration(self) -> None:
        if not self.keeper_params.session_token:
            demisto.debug("No session token configured")
            raise DemistoException(REGISTRATION_FLOW_MESSAGE)
        self.query_audit_logs(limit=1, start_event_time=0)


def load_json(path: str):
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


def get_audit_logs(
    client: Client, last_latest_event_time: int, max_fetch_limit: int, last_fetched_ids: set[str]
) -> list[dict[str, Any]]:
    continue_fetching = True
    events_to_return: list[dict[str, Any]] = []
    # last_latest_event_time -> UNIX epoch time in seconds
    start_time_to_fetch = last_latest_event_time
    fetched_ids = last_fetched_ids
    res_count = 0
    while continue_fetching and res_count < max_fetch_limit:
        query_response = client.query_audit_logs(
            limit=min(API_MAX_FETCH, max_fetch_limit - res_count), start_event_time=start_time_to_fetch
        )
        audit_events = query_response["audit_event_overview_report_rows"]
        audit_events_count = len(audit_events)
        demisto.debug(f"{LOG_LINE} got {audit_events_count} events from API")
        if audit_events:
            dedupped_audit_events = dedup_events(audit_events, fetched_ids)
            dedupped_events_count = len(dedupped_audit_events)
            demisto.debug(f"{LOG_LINE} Events count after dedup {dedupped_events_count}")
            if dedupped_audit_events:
                add_time_to_events(dedupped_audit_events)
                res_count += dedupped_events_count
                events_to_return.extend(dedupped_audit_events)
                # Getting last events's creation date, assuming asc order
                start_time_to_fetch = int(dedupped_audit_events[-1]["created"])
                # We get the event IDs that have the same creation time as the latest event in the response
                # We use them to dedup in the next run
                fetched_ids = {
                    str(audit_event["id"])
                    for audit_event in dedupped_audit_events
                    if int(audit_event["created"]) == start_time_to_fetch
                }
                # Last run of pagination, avoiding endless loop if all the page's results have the same time.
                # We do not have other eay to handle this case.
                if last_latest_event_time == start_time_to_fetch:
                    demisto.debug("Got equal start and end time, this was the last page.")
                    continue_fetching = False
            else:
                continue_fetching = False
        else:
            continue_fetching = False
    demisto.setLastRun({"last_fetch_epoch_time": str(start_time_to_fetch), "last_fetch_ids": list(fetched_ids)})
    return events_to_return


def add_time_to_events(audit_events: list[dict[str, Any]]):
    for audit_event in audit_events:
        audit_event["_time"] = audit_event["created"]


def dedup_events(audit_events: list[dict[str, Any]], last_fetched_ids: set[str]) -> list[dict[str, Any]]:
    dedupped_audit_events = list(
        filter(
            lambda audit_event: str(audit_event["id"]) not in last_fetched_ids,
            audit_events,
        )
    )
    return dedupped_audit_events


def fetch_events(client: Client, last_run: dict[str, Any], max_fetch_limit: int) -> list[dict[str, Any]]:
    demisto.debug(f"last_run: {last_run}" if last_run else "last_run is empty")
    # SDK's query uses Epoch time to filter events
    last_fetch_epoch_time = int(last_run.get("last_fetch_epoch_time", "0"))

    # (if 0) returns False
    last_fetch_epoch_time = last_fetch_epoch_time if last_fetch_epoch_time else int(datetime.now().timestamp())
    # last_fetch_epoch_time = 0
    last_fetched_ids = set(last_run.get("last_fetch_ids", []))
    audit_log = get_audit_logs(
        client=client,
        last_latest_event_time=last_fetch_epoch_time,
        max_fetch_limit=max_fetch_limit,
        last_fetched_ids=last_fetched_ids,
    )
    return audit_log


def start_registration_command(client: Client):
    client.start_registration()
    return CommandResults(readable_output="Code was sent successfully to the user's email")


def complete_registration_command(client: Client, code: str):
    client.complete_registration(code=code)
    return CommandResults(readable_output="Login completed")


def test_authorization(
    client: Client,
) -> CommandResults:
    client.test_registration()
    return CommandResults(readable_output="Successful connection")


def test_module() -> str:
    # We are unable to use client.test_registration(), since the method uses the integration context
    # and when we are running test-module, we don't have access to it
    raise DemistoException(REGISTRATION_FLOW_MESSAGE)


def main() -> None:  # pragma: no cover
    """main function, parses params and runs command functions"""
    params = demisto.params()
    command = demisto.command()
    args = demisto.args()
    username = params.get("credentials", {})["identifier"]
    password = params.get("credentials", {})["password"]
    server_url = params.get("url") or "keepersecurity.com"
    client = Client(
        server_url=server_url,
        username=username,
        password=password,
    )
    client.refresh_session_token_if_needed()
    demisto.debug(f"Command being called is {demisto.command()}")
    try:
        if command == "test-module":
            return_results(test_module())
        elif command == "keeper-security-register-start":
            return_results(start_registration_command(client=client))
        elif command == "keeper-security-register-complete":
            return_results(complete_registration_command(client=client, code=args.get("code", "")))
        elif command == "keeper-security-register-test":
            return_results(test_authorization(client=client))
        elif command == "fetch-events":
            last_run = demisto.getLastRun()
            fetched_audit_logs = fetch_events(
                client=client,
                last_run=last_run,
                max_fetch_limit=arg_to_number(params.get("alerts_max_fetch")) or DEFAULT_MAX_FETCH,
            )
            demisto.debug(f"Events to send to XSIAM {fetched_audit_logs=}")
            send_events_to_xsiam(fetched_audit_logs, VENDOR, PRODUCT)
        else:
            raise NotImplementedError
    # Log exceptions and return errors
    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()