Bitwarden Password Manager
This integration collects event logs from Bitwarden Password Manager to Cortex XSIAM.
Analytics & SIEM · Bitwarden Password Manager
Details
| ID | Bitwarden Password Manager |
|---|---|
| Provider | Bitwarden |
| Category | Analytics & SIEM |
| From Version | 8.6.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | XSIAM |
README
This integration collects event logs from Bitwarden Password Manager to Cortex XSIAM.
This integration was integrated and tested with version 2024.6.2 of Bitwarden Password Manager.
Configure Bitwarden Password Manager in Cortex
| Parameter | Required |
|---|---|
| Server URL (e.g., https://example.bitwarden.com) | True |
| Self-hosted Bitwarden Instance | False |
| Client ID | True |
| Client Secret | True |
| Trust any certificate (not secure) | False |
| Use system proxy settings | False |
| Maximum number of events per fetch | False |
Note: Enable the “Self-hosted Bitwarden Instance” parameter if you are using a self-hosted Bitwarden server. For self-hosted instances, the authentication endpoint will be constructed from your Server URL (e.g., https://your-domain.com/identity/connect/token). For cloud-hosted instances (default), the integration uses Bitwarden’s cloud authentication endpoints.
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.
bitwarden-get-events
Gets events from Bitwarden.
Base Command
bitwarden-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| start | The start date from which to filter events. (e.g., 2024-07-10T08:08:46.76) | Optional |
| end | The end date from which to filter events. (e.g., 2024-07-11T08:09:47.08) | Optional |
| limit | The number of events to return. Default is 500. | Optional |
| should_push_events | Set this argument to True in order to save events to XSIAM, otherwise the command will only display them. Possible values are: true, false. Default is false. | Optional |
Context Output
There is no context output for this command.
Configuration parameters
url— Server URL (e.g., https://example.bitwarden.com) (required)self_hosted— Self-hosted Bitwarden Instancecredentials— Client ID (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsmax_fetch_events— Maximum number of events per fetch
Commands (1)
-
bitwarden-get-eventsGets events from Bitwarden.
import hashlib import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 from CommonServerUserPython import * # noqa VENDOR = "Bitwarden" PRODUCT = "Password Manager" DEFAULT_MAX_FETCH = 500 SECONDS_BEFORE_TOKEN_EXPIRED = 120 AUTHENTICATION_FULL_URL = "https://identity.bitwarden.com/connect/token" DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" DEFAULT_FIRST_FETCH = (get_current_time() - timedelta(minutes=1)).strftime(DATE_FORMAT) DEFAULT_END_DATE = (get_current_time() + timedelta(days=1)).strftime(DATE_FORMAT) class Client(BaseClient): """ Client class to interact with the service API """ def __init__(self, base_url: str, verify: bool, proxy: bool, client_id: str, client_secret: str, self_hosted: bool = False): super().__init__(base_url=base_url, verify=verify, proxy=proxy) self.self_hosted = self_hosted self.token = self.login(client_id, client_secret) def login(self, client_id: str, client_secret: str) -> str: integration_context = get_integration_context() if token := integration_context.get("token"): expires_date = integration_context.get("expires") if expires_date and not self.is_token_expired(expires_date): return token json_data = { "client_id": client_id, "client_secret": client_secret, "grant_type": "client_credentials", "scope": "api.organization", } return self.create_new_token(json_data) def is_token_expired(self, expires_date: str) -> bool: utc_now = get_current_time() expires_datetime = arg_to_datetime(expires_date) return utc_now > expires_datetime def create_new_token(self, json_data: dict) -> str: if self.self_hosted: # For self-hosted instances, construct URL from base_url full_url = urljoin(self._base_url, "/identity/connect/token") else: # For cloud-hosted instances, use existing logic (supports EU region) full_url = AUTHENTICATION_FULL_URL.replace(".com", ".eu") if ".eu" in self._base_url else AUTHENTICATION_FULL_URL demisto.debug(f"Authenticating to {'self-hosted' if self.self_hosted else 'cloud'} Bitwarden instance at {full_url}") access_token_obj = self._http_request( method="POST", full_url=full_url, headers={"Content-Type": "application/x-www-form-urlencoded"}, data=json_data, ) new_access_token = access_token_obj.get("access_token", "") expire_in = arg_to_number(access_token_obj.get("expires_in")) or 1 self.store_token_in_context(new_access_token, expire_in) return new_access_token def store_token_in_context(self, token: str, expire_in: int) -> None: expire_date = get_current_time() + timedelta(seconds=expire_in) - timedelta(seconds=SECONDS_BEFORE_TOKEN_EXPIRED) set_integration_context(context={"token": token, "expires": str(expire_date)}) def get_events(self, start_date: str = "", end_date: str = "", continuation_token: str = "") -> dict: demisto.debug(f"Bitwarden - get-events from {start_date=} to {end_date=}") params = {"start": start_date, "end": end_date} if continuation_token: params["continuationToken"] = continuation_token headers = {"Authorization": f"Bearer {self.token}"} url_suffix = "/public/events" if self.self_hosted: url_suffix = f"/api{url_suffix}" res = self._http_request(method="GET", url_suffix=url_suffix, headers=headers, params=params) return res def test_module(client: Client) -> str: fetch_events(client, max_fetch=1) return "ok" def get_events_command(client: Client, args: Dict[str, Any]) -> tuple: limit = arg_to_number(args.get("limit", DEFAULT_MAX_FETCH)) or DEFAULT_MAX_FETCH start = args.get("start", DEFAULT_FIRST_FETCH) end = args.get("end", DEFAULT_END_DATE) events, _ = fetch_events(client=client, max_fetch=limit, dates={"start": start, "end": end}) if events: events = events[:limit] return events, CommandResults(readable_output=tableToMarkdown("Bitwarden Events", events), raw_response=events) return [], CommandResults(readable_output="No events found") def fetch_events( client: Client, max_fetch: int, dates: Dict[str, Any] = {"start": DEFAULT_FIRST_FETCH, "end": DEFAULT_END_DATE} ) -> tuple[List[Dict[str, Any]], Dict[str, Any]]: """Fetches events from the API using the provided client. Args: - client (Client): The client object used to make API requests. - max_fetch (int): The maximum number of events to fetch. - dates (Dict[str, Any], optional): A dictionary containing the start and end dates for the events. The default values are set to DEFAULT_FIRST_FETCH for the start date and DEFAULT_END_DATE for the end date. Returns: - tuple[List[Dict[str, Any]], Dict[str, Any]]: A tuple containing a list of fetched events and a last_run object. - The list of events contains dictionaries with event information. - The last_run object contains the new last fetch date and, if there is a continuationToken, nextTrigger is set to 0. Additional Functionality: - The function calls the get_events_with_pagination function, which internally calls client.get_events to fetch events from the API. - The function checks whether the events fetched in the most recent request are identical to the oldest events fetched in the subsequent request. This check is implemented to prevent duplicate events from being included in the fetched results. """ last_run = demisto.getLastRun() demisto.debug(f"Bitwarden - fetch-events {last_run=}") events, continuation_token = get_events_with_pagination(client, max_fetch, dates, last_run) if not events: demisto.debug("Bitwarden - No events were found.") return [], last_run unique_events = get_unique_events(events, last_run) recent_events = filter_events(events=events, oldest=False) hashed_recent_events = hash_events(recent_events) if continuation_token: demisto.debug( f"Bitwarden - Fetched {len(unique_events)} which is the maximum or greater then the number of events." f" Will keep the fetching in the next fetch." ) last_fetch_date = unique_events[0].get("date", "").split("Z")[0] split_string = last_fetch_date.split(".") formatted_datetime = split_string[0] + "." + split_string[1][:-4].ljust(3, "0") new_last_run = { "continuationToken": continuation_token, "last_fetch": formatted_datetime, "nextTrigger": "0", "hashed_recent_events": hashed_recent_events, } else: # If there is no continuation token, the last fetch date will be the max end date of the fetched events. new_last_fetch_date = ( max( [dt for dt in (arg_to_datetime(event.get("date"), DATE_FORMAT) for event in unique_events) if dt is not None] ).strftime(DATE_FORMAT) if unique_events else get_current_time() ) new_last_run = {"last_fetch": new_last_fetch_date, "hashed_recent_events": hashed_recent_events} demisto.debug(f"Bitwarden - Fetched {len(unique_events)} events") for event in unique_events: event["_time"] = event.get("date") return unique_events, new_last_run def get_events_with_pagination( client: Client, max_fetch: int, dates: Dict[str, Any], last_run: Dict[str, Any] ) -> tuple[List[Dict[str, Any]], str]: continuation_token = last_run.get("continuationToken", "") events: List[dict] = [] has_next = True while has_next: has_next = False if len(events) >= max_fetch: break start_date = last_run.get("last_fetch", "") if last_run.get("last_fetch", "") else dates.get("start", DEFAULT_FIRST_FETCH) response = client.get_events( start_date=start_date, end_date=dates.get("end", DEFAULT_END_DATE), continuation_token=continuation_token ) if continuation_token := response.get("continuationToken"): has_next = True events.extend(response.get("data", [])) return events, continuation_token def get_unique_events(events: List[Dict[str, Any]], last_run: Dict[str, Any]) -> List[Dict[str, Any]]: if last_fetched_hashed_recent_events := last_run.get("hashed_recent_events"): oldest_events = filter_events(events=events, oldest=True) hashed_oldest_events = hash_events(oldest_events) should_be_removed_events = [] for hashed_oldest_event, oldest_event in hashed_oldest_events.items(): if hashed_oldest_event in list(last_fetched_hashed_recent_events.keys()): should_be_removed_events.append(oldest_event) events_to_remove_set = {tuple(event.items()) for event in should_be_removed_events} unique_events = [event for event in events if tuple(event.items()) not in events_to_remove_set] return unique_events return events def filter_events(events: List[Dict[str, Any]], oldest: bool) -> List[Dict[str, Any]]: sorted_events = sorted(events, key=lambda x: x["date"]) date = sorted_events[0]["date"] if oldest else sorted_events[-1]["date"] filtered_events = [event for event in sorted_events if event["date"] == date] return filtered_events def hash_events(events: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: hashed_events = {} for event in events: event_str = json.dumps(event, sort_keys=True) event_hash_object = hashlib.sha256(event_str.encode()).hexdigest() hashed_events[event_hash_object] = event return hashed_events def main() -> None: # pragma: no cover """ main function, parses params and runs command functions :return: :rtype: """ demisto_params = demisto.params() base_url = demisto_params.get("url", "https://api.bitwarden.com") client_id = demisto_params.get("credentials", {}).get("identifier") client_secret = demisto_params.get("credentials", {}).get("password") max_events_per_fetch = arg_to_number(demisto_params.get("max_fetch_events")) or DEFAULT_MAX_FETCH verify_certificate = not demisto_params.get("insecure", False) proxy = demisto_params.get("proxy", False) self_hosted = demisto_params.get("self_hosted", False) command = demisto.command() demisto.debug(f"Command being called is {command}") try: client = Client( base_url=base_url, verify=verify_certificate, client_id=client_id, client_secret=client_secret, proxy=proxy, self_hosted=self_hosted, ) args = demisto.args() if command == "test-module": return_results(test_module(client)) elif command == "bitwarden-get-events": events, results = get_events_command(client=client, args=args) return_results(results) if argToBoolean(args.get("should_push_events", False)): send_events_to_xsiam(events=events, vendor=VENDOR, product=PRODUCT) elif demisto.command() == "fetch-events": events, new_last_run = fetch_events(client=client, max_fetch=max_events_per_fetch) if events: send_events_to_xsiam(events=events, vendor=VENDOR, product=PRODUCT) demisto.setLastRun({"last_fetch": new_last_run.get("last_fetch")}) except Exception as e: return_error(f"Failed to execute {command} command.\nError:\n{e!s}") if __name__ in ("__main__", "__builtin__", "builtins"): main()