Bitwarden Password Manager

This integration collects event logs from Bitwarden Password Manager to Cortex XSIAM.

Analytics & SIEM · Bitwarden Password Manager

Details

IDBitwarden Password Manager
ProviderBitwarden
CategoryAnalytics & SIEM
From Version8.6.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesXSIAM

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 Instance
  • credentials — Client ID (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • max_fetch_events — Maximum number of events per fetch

Commands (1)

  • bitwarden-get-events

    Gets events from Bitwarden.

import json

import pytest
from BitwardenPasswordManager import Client
from freezegun import freeze_time

from Packs.BitwardenPasswordManager.Integrations.BitwardenPasswordManager import BitwardenPasswordManager

MOCK_BASEURL = "https://mock.api.com"
MOCK_CLIENT_ID = "mock_client_id"
MOCK_CLIENT_SECRET = "mock_secret"


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


@pytest.fixture(autouse=True)
@freeze_time("2024-04-25 00:00:00")
def mock_client_with_valid_token(mocker) -> Client:
    """
    Establish a connection to the client with a user credentials.
    This client contains a valid token.

    Returns:
        Client: Connection to client.
    """

    mocker.patch(
        "BitwardenPasswordManager.get_integration_context", return_value={"token": "access_token", "expires": "1715032135"}
    )

    return Client(
        base_url=MOCK_BASEURL,
        verify=False,
        client_id=MOCK_CLIENT_ID,
        client_secret=MOCK_CLIENT_SECRET,
        proxy=False,
        self_hosted=False,
    )


def test_login_when_token_creation(mocker):
    """
    Given: An empty integration context
    When: Login is called and token not exist or invalid.
    Then: Create a new token and save it to the integration context
    """

    mock_response = util_load_json("test_data/mock_response_login_token_creation.json")
    mocker.patch.object(Client, "_http_request", return_value=mock_response)
    mocker.patch("BitwardenPasswordManager.get_integration_context", return_value={})

    client = Client(
        base_url=MOCK_BASEURL,
        verify=False,
        client_id=MOCK_CLIENT_ID,
        client_secret=MOCK_CLIENT_SECRET,
        proxy=False,
        self_hosted=False,
    )

    assert client.token == "access_token"


@freeze_time("2024-04-25 00:00:00")
def test_login_with_valid_token(mock_client_with_valid_token):
    """
    Given: A token in the integration context with a valid expiration time
    When: Login is called with a valid token
    Then: Fetch the token from the integration context and log in
    """

    assert mock_client_with_valid_token.token == "access_token"


@freeze_time("2024-04-25 00:00:00")
@pytest.mark.parametrize(
    "base_url, full_url",
    [
        (MOCK_BASEURL, "https://identity.bitwarden.com/connect/token"),
        ("https://mock.api.eu", "https://identity.bitwarden.eu/connect/token"),
    ],
)
def test_create_new_token(mocker, base_url: str, full_url: str):
    """
    Given: A client and authentication data
    When: create_new_token is called with valid credentials
    Then: A new token is created, stored in context, and returned
    """
    from BitwardenPasswordManager import Client

    # Mock the HTTP response for token creation
    mock_response = util_load_json("test_data/mock_response_login_token_creation.json")
    mock_http_request = mocker.patch.object(Client, "_http_request", return_value=mock_response)
    mock_set_context = mocker.patch("BitwardenPasswordManager.set_integration_context")
    mock_get_current_time = mocker.patch("BitwardenPasswordManager.get_current_time")

    # Set a fixed time for consistent testing
    from datetime import datetime

    fixed_time = datetime(2024, 4, 25, 0, 0, 0)
    mock_get_current_time.return_value = fixed_time

    # Create client instance without calling login (to test create_new_token directly)
    client = Client.__new__(Client)
    client._base_url = base_url
    client._verify = False
    client._proxy = False
    client.self_hosted = False

    # Test data for token creation
    json_data = {
        "client_id": MOCK_CLIENT_ID,
        "client_secret": MOCK_CLIENT_SECRET,
        "grant_type": "client_credentials",
        "scope": "api.organization",
    }

    # Call the method under test
    result_token = client.create_new_token(json_data)

    # Verify the HTTP request was made with correct parameters
    mock_http_request.assert_called_once_with(
        method="POST",
        full_url=full_url,
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        data=json_data,
    )

    # Verify the token was returned correctly
    assert result_token == "access_token"

    # Verify the context was set with the token and expiration
    mock_set_context.assert_called_once()
    context_call_args = mock_set_context.call_args[1]["context"]
    assert context_call_args["token"] == "access_token"
    assert "expires" in context_call_args


@freeze_time("2024-04-25 00:00:00")
@pytest.mark.parametrize(
    "base_url, expected_url",
    [
        ("https://vault.customer.com", "https://vault.customer.com/identity/connect/token"),
        ("https://vault.customer.com/", "https://vault.customer.com/identity/connect/token"),
        ("https://bitwarden.example.org", "https://bitwarden.example.org/identity/connect/token"),
    ],
)
def test_create_new_token_self_hosted(mocker, base_url: str, expected_url: str):
    """
    Given: A client configured for a self-hosted Bitwarden instance
    When: create_new_token is called with self_hosted=True
    Then: The authentication URL is constructed from the base_url
    """
    from BitwardenPasswordManager import Client

    # Mock the HTTP response for token creation
    mock_response = util_load_json("test_data/mock_response_login_token_creation.json")
    mock_http_request = mocker.patch.object(Client, "_http_request", return_value=mock_response)
    mock_set_context = mocker.patch("BitwardenPasswordManager.set_integration_context")
    mock_get_current_time = mocker.patch("BitwardenPasswordManager.get_current_time")

    # Set a fixed time for consistent testing
    from datetime import datetime

    fixed_time = datetime(2024, 4, 25, 0, 0, 0)
    mock_get_current_time.return_value = fixed_time

    # Create client instance with self_hosted=True
    client = Client.__new__(Client)
    client._base_url = base_url
    client._verify = False
    client._proxy = False
    client.self_hosted = True

    # Test data for token creation
    json_data = {
        "client_id": MOCK_CLIENT_ID,
        "client_secret": MOCK_CLIENT_SECRET,
        "grant_type": "client_credentials",
        "scope": "api.organization",
    }

    # Call the method under test
    result_token = client.create_new_token(json_data)

    # Verify the HTTP request was made with the self-hosted URL
    mock_http_request.assert_called_once_with(
        method="POST",
        full_url=expected_url,
        headers={"Content-Type": "application/x-www-form-urlencoded"},
        data=json_data,
    )

    # Verify the token was returned correctly
    assert result_token == "access_token"

    # Verify the context was set with the token and expiration
    mock_set_context.assert_called_once()
    context_call_args = mock_set_context.call_args[1]["context"]
    assert context_call_args["token"] == "access_token"
    assert "expires" in context_call_args


@freeze_time("2024-04-25 00:00:00")
def test_client_initialization_with_self_hosted(mocker):
    """
    Given: Configuration parameters for a self-hosted Bitwarden instance
    When: Client is initialized with self_hosted=True
    Then: The client is created with the self_hosted flag set correctly
    """
    from BitwardenPasswordManager import Client

    # Mock the HTTP response for token creation
    mock_response = util_load_json("test_data/mock_response_login_token_creation.json")
    mocker.patch.object(Client, "_http_request", return_value=mock_response)
    mocker.patch("BitwardenPasswordManager.get_integration_context", return_value={})
    mocker.patch("BitwardenPasswordManager.set_integration_context")

    # Create client with self_hosted=True
    client = Client(
        base_url="https://vault.customer.com",
        verify=False,
        client_id=MOCK_CLIENT_ID,
        client_secret=MOCK_CLIENT_SECRET,
        proxy=False,
        self_hosted=True,
    )

    # Verify the self_hosted flag is set
    assert client.self_hosted is True
    assert client.token == "access_token"


@freeze_time("2024-04-25 00:00:00")
def test_client_initialization_without_self_hosted(mocker):
    """
    Given: Configuration parameters for a cloud-hosted Bitwarden instance
    When: Client is initialized without self_hosted parameter (defaults to False)
    Then: The client is created with the self_hosted flag set to False (backward compatibility)
    """
    from BitwardenPasswordManager import Client

    # Mock the HTTP response for token creation
    mock_response = util_load_json("test_data/mock_response_login_token_creation.json")
    mocker.patch.object(Client, "_http_request", return_value=mock_response)
    mocker.patch("BitwardenPasswordManager.get_integration_context", return_value={})
    mocker.patch("BitwardenPasswordManager.set_integration_context")

    # Create client without self_hosted parameter (should default to False)
    client = Client(base_url=MOCK_BASEURL, verify=False, client_id=MOCK_CLIENT_ID, client_secret=MOCK_CLIENT_SECRET, proxy=False)

    # Verify the self_hosted flag defaults to False
    assert client.self_hosted is False
    assert client.token == "access_token"


@freeze_time("2024-04-25 00:00:00")
def test_get_events_with_limit(mock_client_with_valid_token, mocker):
    """
    Given: A mock BitwardenPasswordManager client.
    When: Running get-events with a limit of 2, while there are more than 2 events.
    Then: Ensure only two events is returned per type.
    """
    from BitwardenPasswordManager import get_events_command

    limit = 2
    raw_response = util_load_json("test_data/raw_response.json")
    mocker.patch.object(Client, "_http_request", return_value=raw_response)
    events, _ = get_events_command(client=mock_client_with_valid_token, args={"limit": limit})
    assert len(events) == limit


def test_filter_oldest_events():
    """
    Given: A mock events.
    When: Running filter_events when using oldest=True.
    Then: Ensure that return the oldest events from the events list.
    """
    from BitwardenPasswordManager import filter_events

    raw_response = util_load_json("test_data/raw_response.json")
    events = raw_response.get("data")
    filtered_events = filter_events(events, oldest=True)

    assert len(filtered_events) == 2
    for event in filtered_events:
        assert event.get("date") == "2020-10-31T15:01:21.698Z"

    assert filtered_events[0].get("type") == 1000
    assert filtered_events[1].get("type") == 1007


def test_filter_recent_events():
    """
    Given: A mock events.
    When: Running filter_events when using oldest=False.
    Then: Ensure that return the recent events from the events list.
    """
    from BitwardenPasswordManager import filter_events

    raw_response = util_load_json("test_data/raw_response.json")
    events = raw_response.get("data")
    filtered_events = filter_events(events, oldest=False)

    assert len(filtered_events) == 2
    for event in filtered_events:
        assert event.get("date") == "2020-11-04T15:01:21.698Z"

    assert filtered_events[0].get("type") == 1000
    assert filtered_events[1].get("type") == 1002


def test_hash_events():
    """
    Given: A mock events.
    When: Running hash_events.
    Then: Ensure that the return dictionary is structured such that each item's value is the event, and the key is the hash
    value of the event.
    """
    from BitwardenPasswordManager import hash_events

    raw_response = util_load_json("test_data/raw_response.json")
    events = raw_response.get("data")
    hashed_events = hash_events(events)
    hashed_first_event = list(hashed_events.keys())[0]
    assert len(hashed_events) == len(events)
    assert list(hashed_events.values()) == events
    assert hashed_first_event == "b6142853d9719c4c6301a5012e42437cb9c6726fcfa5b930bd3be6b7048a0d53"
    assert len(hashed_first_event) == 64


def test_get_unique_events_with_duplicates():
    """
    Given: A mock events.
    When: Run the function get_unique_events with the condition that the events should include items that appeared
    in the previous last_run.
    Then: Ensure that the return list contain only unique items.
    """
    from BitwardenPasswordManager import get_unique_events

    raw_response = util_load_json("test_data/raw_response.json")
    events = raw_response.get("data")
    mock_last_run = {
        "hashed_recent_events": {
            "e6bff23ab05c63226e4ad2b15a5713589ab59f01a37cec6f731bad7886a77634": {
                "object": "event",
                "type": 1007,
                "itemId": "event_with_the_same_date_as_other_event",
                "collectionId": "string",
                "groupId": "string",
                "policyId": "string",
                "memberId": "string",
                "actingUserId": "string",
                "date": "2020-10-31T15:01:21.698Z",
                "device": 0,
                "ipAddress": "xxx.xx.xxx.x",
            }
        }
    }
    unique_events = get_unique_events(events, mock_last_run)
    assert list(mock_last_run.get("hashed_recent_events").values())[0] not in unique_events
    assert len(unique_events) == len(events) - 1


def test_get_unique_events_without_duplicates():
    """
    Given: A mock events.
    When: Run the function get_unique_events with the condition that the events are new and not
    Then: Ensure that the return list contain only unique items, and it equals to the events list.
    """
    from BitwardenPasswordManager import get_unique_events

    raw_response = util_load_json("test_data/raw_response.json")
    events = raw_response.get("data")
    mock_last_run = {
        "hashed_recent_events": {
            "dddff23ab05c63226e4ad2b15a5713589ab59f01a37cec6f731bad7886a77634": {
                "object": "event",
                "type": 1007,
                "itemId": "event_with_the_same_date_as_other_event",
                "collectionId": "string",
                "groupId": "string",
                "policyId": "string",
                "memberId": "string",
                "actingUserId": "string",
                "date": "2020-10-30T15:01:21.698Z",
                "device": 0,
                "ipAddress": "xxx.xx.xxx.x",
            }
        }
    }
    unique_events = get_unique_events(events, mock_last_run)
    assert unique_events == events


@freeze_time("2024-04-25 00:00:00")
def test_get_events_with_pagination(mock_client_with_valid_token, mocker):
    """
    Given: A mock events.
    When: Execute the 'get_events_with_pagination' function when the events are divided into two lists: the first list contains
    events with a continuation token, and the second list does not include a continuation token.
    Then: Ensure that the return list contain all the events from both lists.
    """
    from BitwardenPasswordManager import get_events_with_pagination

    raw_response_with_continuationToken = util_load_json("test_data/raw_response_with_continuationToken.json")
    raw_response = util_load_json("test_data/raw_response.json")
    mocker.patch.object(Client, "_http_request", side_effect=[raw_response_with_continuationToken, raw_response])
    events, continuation_token = get_events_with_pagination(
        mock_client_with_valid_token, BitwardenPasswordManager.DEFAULT_MAX_FETCH, {}, {}
    )

    assert len(events) == len(raw_response.get("data")) + len(raw_response_with_continuationToken.get("data"))


@freeze_time("2024-04-25 00:00:00")
def test_fetch_events_without_continuation_token(mock_client_with_valid_token, mocker):
    """
    Given: A mock events.
    When: Execute the 'fetch_events' such that the fetch successes in one shot and no need for continuation token.
    Then: Ensure that the return list contain all the events and that the new_last_run object contain the date of the latest
    fetched event.
    """
    from BitwardenPasswordManager import fetch_events

    raw_response = util_load_json("test_data/raw_response.json")
    mocker.patch.object(Client, "_http_request", return_value=raw_response)
    unique_events, new_last_run = fetch_events(mock_client_with_valid_token, BitwardenPasswordManager.DEFAULT_MAX_FETCH, {})
    assert len(unique_events) == len(raw_response.get("data"))
    assert new_last_run.get("last_fetch")[:-3] == raw_response.get("data")[0].get("date")[:-1]
    assert list(new_last_run.get("hashed_recent_events").values())[0] == raw_response.get("data")[0]


@freeze_time("2024-04-25 00:00:00")
def test_fetch_events_with_continuation_token(mock_client_with_valid_token, mocker):
    """
    Given: A mock events.
    When: Execute the 'fetch_events' function with the condition that the number of events exceeds the 'max_fetch' limit,
     triggering the need for a new fetch operation and returning a continuation token.
    Then: Ensure that returned a continuation token and the nextTrigger is set to 0.
    fetched event.
    """
    from BitwardenPasswordManager import fetch_events

    raw_response_with_continuationToken = util_load_json("test_data/raw_response_with_continuationToken.json")
    mocker.patch.object(Client, "_http_request", return_value=raw_response_with_continuationToken)
    unique_events, new_last_run = fetch_events(mock_client_with_valid_token, max_fetch=1, dates={})
    assert new_last_run.get("continuationToken") == "continuation_token"
    assert new_last_run.get("nextTrigger") == "0"