IBMMaaS360Security

This is the IBM MaaS360 Security event collector integration for Cortex XSIAM.

Analytics & SIEM · IBM MaaS360 Security

Details

IDIBMMaaS360Security
ProviderIBM
CategoryAnalytics & SIEM
From Version8.4.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesXSIAM

README

This is the IBM MaaS360 Security integration for Cortex XSIAM.

Create App credentials

To access the IBM MaaS360 Security API, you first need to generate an App access key.
Follow these steps:

  1. Log in to the IBM MaaS360 portal.

  2. Go to Setup > Manage Access Keys.
    Generate access key

  3. Click Generate Access Key.
    Generate access key

  4. Create a new MaaS360 Web Services key.
    Generate access key

  5. Get App ID, App Version, Platform ID, and App Access Key.
    Generate access key

For further assistance, you may contact IBM MaaS360 support to request these credentials as well.
See Configuring provisioning for MaaS360

Find your Billing ID

  1. Log in to the IBM MaaS360 portal.

  2. Hover over the profile menu on the top right and copy the Account ID value. This is your Billing ID.
    Get Billing ID

Account permissions

To collect events, the user requires read-only permissions.
Follow these steps to modify user permissions:

  1. Log in to the IBM MaaS360 portal.

  2. Go to Setup > Administrators
    Admin permissions

  3. Find the relevant administrator account or create a new one.

  4. Click Edit. (Note: This does not appear for the primary admin account.)
    Admin permissions

  5. Add the Read-Only permission.
    Admin permissions

Configure IBM MaaS360 Security on Cortex XSIAM

  1. Navigate to Settings > Configurations > Data Collection > Automations & Feed Integrations.
  2. Search for IBM MaaS360 Security.
  3. Click Add instance to create and configure a new integration instance.

    Parameter Description Required
    Server URL   True
    User Name   True
    Password   True
    App ID   True
    App Version   True
    Platform ID   True
    App Access Key   True
    Billing ID   True
    Maximum number of Admin Login Reports events per fetch Rounds up to the nearest page size increment. (250) False
    Maximum number of Admin Changes Audit events per fetch Rounds up to the nearest page size increment. (250) False
    Trust any certificate (not secure)   False
    Use system proxy settings   False
  4. Click Test to validate the URLs, token, and connection.

Commands

You can execute these commands from the Cortex XSIAM 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.

ibm-maas360-security-get-events


Gets events from IBM MaaS360 Security.

Base Command

ibm-maas360-security-get-events

Input

Argument Name Description Required
should_push_events If true, the command will create events, otherwise it will only display them. Possible values are: true, false. Default is false. Required
limit Maximum number of results to return. Required
from_date Date from which to get events. Optional

Context Output

There is no context output for this command.

Human Readable Output

Admin audits

_time source_log_type updateDate performedBy operationType ipAddress administrator
2024-10-29T13:39:32Z admin_changes_audit 1730209172000 test@mail.com CHANGE 1.1.1.1 email: test@mail.com
username: test@mail.com
firstName: tester
middleName:
lastName: tester
address:
phone:
jobTitle:
watchSubscription: Y
accountStatus: Active
managedUserGroups: All groups
lastLoginTime:
rolesAdded:
rolesDeleted:
previousState:
2024-10-29T13:39:32Z admin_changes_audit 1730209172000 test@mail.com CHANGE 1.1.1.1 email: test@mail.com
username: test@mail.com
firstName: tester
middleName:
lastName: tester
address:
phone:
jobTitle:
watchSubscription: Y
accountStatus: Active
managedUserGroups: All groups
lastLoginTime:
rolesAdded: Help Desk
rolesDeleted:
previousState:
2024-10-29T12:53:49Z admin_changes_audit 1730206429000 test@mail.com CHANGE 1.1.1.1 email: test@mail.com
username: test@mail.com
firstName: tester
middleName:
lastName: tester
address:
phone:
jobTitle:
watchSubscription: Y
accountStatus: Active
managedUserGroups: All groups
lastLoginTime:
rolesAdded:
rolesDeleted:
previousState:
2024-10-29T12:53:49Z admin_changes_audit 1730206429000 test@mail.com CHANGE 1.1.1.1 email: test@mail.com
username: test@mail.com
firstName: tester
middleName:
lastName: tester
address:
phone:
jobTitle:
watchSubscription: Y
accountStatus: Active
managedUserGroups: All groups
lastLoginTime:
rolesAdded:
rolesDeleted: Help Desk
previousState:
2024-10-29T12:41:31Z admin_changes_audit 1730205691000 test@mail.com CHANGE 1.1.1.1 email: test@mail.com
username: test@mail.com
firstName: tester
middleName:
lastName: tester
address:
phone:
jobTitle:
watchSubscription: Y
accountStatus: Active
managedUserGroups: All groups
lastLoginTime:
rolesAdded:
rolesDeleted:
previousState:

Configuration parameters

  • url — Server URL (required)
  • credentials — User Name (required)
  • app_id — App ID (required)
  • app_version — App Version (required)
  • platform_id — Platform ID (required)
  • app_access_key — (required)
  • billing_id — (required)
  • max_login_reports_per_fetch — Maximum number of Admin Login Reports events per fetch
  • max_admin_change_audits_per_fetch — Maximum number of Admin Changes Audit events per fetch
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • ibm-maas360-security-get-events

    Gets events from IBM MaaS360 Security.

import json
from datetime import datetime, timedelta
from math import ceil

import pytest
from CommonServerPython import date_to_timestamp, set_integration_context
from dateutil import tz
from freezegun import freeze_time
from IBMMaaS360SecurityEventCollector import DATE_FORMAT, AuditEventType, Client

PAGE_SIZE = 3
BILLING_ID = "testbillingid"
BASE_URL = "https://test.com"
AUTH_SUFFIX = f"/auth-apis/auth/2.0/authenticate/customer/{BILLING_ID}"
REFRESH_SUFFIX = f"/auth-apis/auth/2.0/refreshToken/customer/{BILLING_ID}"
AUDIT_CHANGES_SUFFIX = AuditEventType.ChangesAudit.url_suffix.format(billingId=BILLING_ID)
AUDIT_LOGIN_REPORTS_SUFFIX = AuditEventType.LoginReports.url_suffix.format(billingId=BILLING_ID)
AUTH_TOKEN = "test_auth_token"
REFRESH_TOKEN = "test_refresh_token"
TEST_TIME = datetime(2024, 10, 30, 12, 0, 0)


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


admin_changes = util_load_json("test_data/mock_admin_changes.json")
login_reports = util_load_json("test_data/mock_login_events.json")


@pytest.fixture(autouse=True)
def test_setup(mocker):
    mocker.patch("IBMMaaS360SecurityEventCollector.PAGE_SIZE", PAGE_SIZE)
    set_integration_context({})


@pytest.fixture
def client() -> Client:
    return Client(
        base_url=BASE_URL,
        username="testuser",
        password="testpassword",
        app_id="testappid",
        app_version="1.0",
        platform_id="testplatform",
        access_key="testkey",
        billing_id=BILLING_ID,
    )


@pytest.fixture
def admin_changes_request(requests_mock):
    def mock_admin_changes_resp(request, context):
        if request.headers.get("Authorization") != f'MaaS token="{AUTH_TOKEN}"':
            context.status_code = 401
            return {"adminChanges": {"errorCode": 1009, "errorDesc": "Token invalid"}}
        page_number = int(request.qs.get("pagenumber", [0])[0])
        if page_number - 1 < len(admin_changes):
            return admin_changes[page_number - 1]
        else:
            return {
                "adminChanges": {
                    "count": 0,
                    "pageNumber": page_number,
                    "pageSize": PAGE_SIZE,
                }
            }

    return requests_mock.get(f"{BASE_URL}{AUDIT_CHANGES_SUFFIX}", json=mock_admin_changes_resp)


@pytest.fixture
def login_reports_request(requests_mock):
    def mock_login_reports_resp(request, context):
        if request.headers.get("Authorization") != f'MaaS token="{AUTH_TOKEN}"':
            return {"adminChanges": {"errorCode": 1009, "errorDesc": "Token invalid"}}
        page_number = int(request.qs.get("pagenumber", [0])[0])
        if page_number - 1 < len(login_reports):
            return login_reports[page_number - 1]
        else:
            return {
                "loginEvents": {
                    "count": 0,
                    "pageNumber": page_number,
                    "pageSize": PAGE_SIZE,
                }
            }

    return requests_mock.get(f"{BASE_URL}{AUDIT_LOGIN_REPORTS_SUFFIX}", json=mock_login_reports_resp)


@pytest.mark.parametrize("error_code", [0, 401, 1008])
def test_authenticate(requests_mock, client, error_code):
    """
    Given:
    - IBM MaaS360 Security client.

    When:
    - client.authenticate is called.

    Then:
    - The authentication request is sent and the auth+refresh tokens are extracted correctly.
    """
    auth_request_mock = requests_mock.post(
        f"{BASE_URL}{AUTH_SUFFIX}",
        json={
            "authResponse": {
                "authToken": AUTH_TOKEN,
                "errorCode": error_code,
                "refreshToken": REFRESH_TOKEN,
            }
        },
    )
    if error_code:
        with pytest.raises(Exception):
            client.authenticate()
    else:
        auth_token, refresh_token = client.authenticate()
        assert auth_request_mock.last_request.json() == {
            "authRequest": {
                "maaS360AdminAuth": {
                    "billingID": BILLING_ID,
                    "platformID": "testplatform",
                    "appID": "testappid",
                    "appVersion": "1.0",
                    "appAccessKey": "testkey",
                    "userName": "testuser",
                    "password": "testpassword",
                }
            }
        }
        assert auth_token == AUTH_TOKEN
        assert refresh_token == REFRESH_TOKEN


@pytest.mark.parametrize("error_code", [0, 401, 1002])
def test_refresh_auth_token(requests_mock, client, error_code):
    """
    Given:
    - IBM MaaS360 Security client.
    - Refresh token.

    When:
    - client.refresh_auth_token is called.

    Then:
    - The token refresh request is sent and the new auth+refresh tokens are extracted correctly.
    """
    refresh_request_mock = requests_mock.post(
        f"{BASE_URL}{REFRESH_SUFFIX}",
        json={
            "authResponse": {
                "authToken": AUTH_TOKEN,
                "errorCode": error_code,
                "refreshToken": f"new_{REFRESH_TOKEN}",
            }
        },
    )
    if error_code:
        with pytest.raises(Exception):
            client.refresh_auth_token(REFRESH_TOKEN)
    else:
        auth_token, refresh_token = client.refresh_auth_token(REFRESH_TOKEN)
        assert refresh_request_mock.last_request.json() == {
            "authRequest": {
                "maaS360AdminAuth": {
                    "platformID": "testplatform",
                    "billingID": BILLING_ID,
                    "userName": "testuser",
                    "appID": "testappid",
                    "appVersion": "1.0",
                    "refreshToken": REFRESH_TOKEN,
                }
            }
        }
        assert auth_token == AUTH_TOKEN
        assert refresh_token == f"new_{REFRESH_TOKEN}"


def test_get_auth_token_first_run(mocker, client):
    """
    Given:
    - IBM MaaS360 Security client.
    - No prior call to get_auth_token.

    When:
    - client.get_auth_token is called.

    Then:
    - The client will authenticate and return the auth token.
    - Consecutive call shortly after won't authenticate again.
    """
    mocked_authenticate = mocker.patch.object(client, "authenticate", return_value=(AUTH_TOKEN, REFRESH_TOKEN))

    auth_token = client.get_auth_token()

    mocked_authenticate.assert_called_once()
    assert auth_token == AUTH_TOKEN

    mocked_authenticate = mocker.patch.object(client, "authenticate", return_value=("bad_token", "bad_refresh"))
    auth_token = client.get_auth_token()

    mocked_authenticate.assert_not_called()
    assert auth_token == AUTH_TOKEN


def test_get_auth_token_long_wait(mocker, client):
    """
    Given:
    - IBM MaaS360 Security client.
    - Prior call to get_auth_token.

    When:
    - client.get_auth_token is called.

    Then:
    - Consecutive call to get_auth_token after token expiry will trigger a token refresh.
    """
    with freeze_time(TEST_TIME) as frozen_time:
        mocked_authenticate = mocker.patch.object(client, "authenticate", return_value=(AUTH_TOKEN, REFRESH_TOKEN))

        auth_token = client.get_auth_token()

        mocked_authenticate.assert_called_once()
        assert auth_token == AUTH_TOKEN

        frozen_time.tick(timedelta(hours=2))  # Token expired

        mocked_authenticate = mocker.patch.object(
            client, "authenticate", return_value=(f"new_{AUTH_TOKEN}", f"new_{REFRESH_TOKEN}")
        )
        mocked_refresh = mocker.patch.object(
            client, "refresh_auth_token", return_value=(f"refreshed_{AUTH_TOKEN}", f"refreshed_{REFRESH_TOKEN}")
        )

        auth_token = client.get_auth_token()

        mocked_authenticate.assert_not_called()
        mocked_refresh.assert_called_once_with(REFRESH_TOKEN)
        assert auth_token == f"refreshed_{AUTH_TOKEN}"


def test_get_auth_token_long_wait_failed_refresh(mocker, requests_mock, client):
    """
    Given:
    - IBM MaaS360 Security client.
    - Prior call to get_auth_token.

    When:
    - client.get_auth_token is called.
    - Token refresh request fails.

    Then:
    - Call to get_auth_token will reauthenticate after token refresh failure.
    """
    with freeze_time() as frozen_time:
        mocked_authenticate = mocker.patch.object(client, "authenticate", return_value=(AUTH_TOKEN, REFRESH_TOKEN))

        auth_token = client.get_auth_token()

        mocked_authenticate.assert_called_once()
        assert auth_token == AUTH_TOKEN

        frozen_time.tick(timedelta(hours=2))  # Token expired

        mocked_authenticate = mocker.patch.object(
            client, "authenticate", return_value=(f"new_{AUTH_TOKEN}", f"new_{REFRESH_TOKEN}")
        )
        refresh_request_mock = requests_mock.post(
            f"{BASE_URL}{REFRESH_SUFFIX}",
            json={
                "authResponse": {
                    "authToken": "bad_token",
                    "errorCode": 1002,
                    "refreshToken": "bad_refresh",
                    "errorDesc": "Invalid credentials",
                }
            },
        )

        auth_token = client.get_auth_token()

        assert refresh_request_mock.called_once
        mocked_authenticate.assert_called_once()
        assert auth_token == f"new_{AUTH_TOKEN}"


def test_http_request(mocker, requests_mock, client):
    """
    Given:
    - IBM MaaS360 Security client.

    When:
    - client.http_request is called.

    Then:
    - An authorization header with a valid auth token will be included in the request.
    """
    mocker.patch.object(client, "get_auth_token", return_value=AUTH_TOKEN)
    admin_changes = util_load_json("test_data/mock_admin_changes.json")[0]
    audit_changes_mock = requests_mock.get(f"{BASE_URL}{AUDIT_CHANGES_SUFFIX}", json=admin_changes)

    res = client.http_request(
        method="GET",
        url_suffix=AUDIT_CHANGES_SUFFIX,
    )

    assert res == admin_changes
    assert audit_changes_mock.last_request.headers["Authorization"] == f'MaaS token="{AUTH_TOKEN}"'


@pytest.mark.parametrize("pages_to_fetch", [1, 2, 3])
def test_fetch_admin_audit_events(mocker, client, admin_changes_request, pages_to_fetch):
    """
    Given:
    - IBM MaaS360 Security client.
    - Fetch params

    When:
    - client.fetch_admin_audit_events is called.

    Then:
    - Events are fetched correctly until there are no more or we reach the max_events_per_fetch.
    - The page_offset and pages_remaining parameters are returned correctly.
    """
    mocker.patch.object(client, "get_auth_token", return_value=AUTH_TOKEN)
    from_date = TEST_TIME - timedelta(days=7)
    to_date = TEST_TIME

    events, page_offset, pages_remaining = client.fetch_admin_audit_events(
        AuditEventType.ChangesAudit,
        from_date=from_date,
        to_date=to_date,
        page_offset=0,
        max_fetch_amount=pages_to_fetch * PAGE_SIZE,
    )

    assert page_offset == pages_to_fetch
    assert pages_remaining if pages_to_fetch < len(admin_changes) else not pages_remaining
    assert admin_changes_request.call_count == pages_to_fetch
    assert admin_changes_request.last_request.qs == {
        "fromdate": [str(from_date)],
        "todate": [str(to_date)],
        "pagesize": [str(PAGE_SIZE)],
        "pagenumber": [str(pages_to_fetch)],
    }

    for event in events:
        event_ts = event[AuditEventType.ChangesAudit.ts_field] / 1000
        expected_time = datetime.fromtimestamp(event_ts, tz=tz.UTC).strftime(DATE_FORMAT)
        assert event.pop("_time") == expected_time
        assert event.pop("source_log_type") == AuditEventType.ChangesAudit.source_log_type

    expected_events = [event for page in admin_changes[:pages_to_fetch] for event in page["adminChanges"]["adminChange"]]
    assert events == expected_events


@pytest.mark.parametrize("max_pages_to_fetch", [1, 2, 3])
def test_fetch_events(mocker, client, admin_changes_request, login_reports_request, max_pages_to_fetch):
    """
    Given:
    - IBM MaaS360 Security client.
    - Last run object.

    When:
    - fetch-events is called multiple times.

    Then:
    - Events are fetched correctly until there are no more or we reach the max_pages_to_fetch.
    - Fetches continue from the previous fetch stopping point.
    - Pagination is done correctly, fetching consecutive pages as expected.
    """
    from IBMMaaS360SecurityEventCollector import fetch_events

    mocker.patch.object(client, "get_auth_token", return_value=AUTH_TOKEN)

    last_run = {}
    first_fetch_time = date_to_timestamp(TEST_TIME - timedelta(days=7))
    max_events_per_fetch = {
        AuditEventType.ChangesAudit: PAGE_SIZE,
        AuditEventType.LoginReports: PAGE_SIZE,
    }

    for i in range(max_pages_to_fetch):
        last_run, events = fetch_events(
            client=client,
            last_run=last_run,
            first_fetch_time=first_fetch_time,
            max_events_per_fetch=max_events_per_fetch,
        )

        expected_admin_changes = []
        expected_login_reports = []
        if i < len(admin_changes):
            assert admin_changes_request.call_count == i + 1
            expected_admin_changes = admin_changes[i]["adminChanges"]["adminChange"]

        if i < len(login_reports):
            assert login_reports_request.call_count == i + 1
            expected_login_reports = login_reports[i]["loginEvents"]["loginEvent"]

        assert len(events) == len(expected_admin_changes) + len(expected_login_reports)

        for event in events:
            event_time = event.pop("_time")
            log_type = event.pop("source_log_type")
            if log_type == AuditEventType.ChangesAudit.source_log_type:
                assert event in expected_admin_changes
                event_ts = event[AuditEventType.ChangesAudit.ts_field] / 1000
            else:
                assert event in expected_login_reports
                event_ts = event[AuditEventType.LoginReports.ts_field] / 1000

            expected_time = datetime.fromtimestamp(event_ts, tz=tz.UTC).strftime(DATE_FORMAT)
            assert event_time == expected_time


def test_test_module_command_success(requests_mock, client, admin_changes_request, login_reports_request):
    """
    Given:
    - IBM MaaS360 Security client.
    - Valid credentials.

    When:
    - Pressing test button

    Then:
    - The test module succeeds in authenticating and fetching data and returns 'ok'.
    """
    from IBMMaaS360SecurityEventCollector import test_module

    first_fetch_time = date_to_timestamp(TEST_TIME - timedelta(days=7))
    auth_request_mock = requests_mock.post(
        f"{BASE_URL}{AUTH_SUFFIX}",
        json={
            "authResponse": {
                "authToken": AUTH_TOKEN,
                "errorCode": 0,
                "refreshToken": REFRESH_TOKEN,
            }
        },
    )

    res = test_module(client, {}, first_fetch_time)

    assert auth_request_mock.called_once
    assert admin_changes_request.called_once
    assert login_reports_request.called_once
    assert res == "ok"


def test_test_module_command_failure(requests_mock, client):
    """
    Given:
    - IBM MaaS360 Security client.
    - Bad credentials.

    When:
    - Pressing test button

    Then:
    - The test module fails with some error message.
    """
    from IBMMaaS360SecurityEventCollector import test_module

    first_fetch_time = date_to_timestamp(TEST_TIME - timedelta(days=7))
    auth_request_mock = requests_mock.post(
        f"{BASE_URL}{AUTH_SUFFIX}",
        json={
            "authResponse": {
                "errorCode": 1002,
                "errorDesc": "Invalid Credentials",
            }
        },
    )

    res = test_module(client, {}, first_fetch_time)

    assert auth_request_mock.called_once
    assert res != "ok"


@pytest.mark.parametrize("limit", [1, 2, 3, 6, 11])
def test_get_events_command(mocker, client, admin_changes_request, login_reports_request, limit):
    """
    Given:
    - IBM MaaS360 Security client.

    When:
    - get-events is called.

    Then:
    - Events are returned as expected.
    """
    from IBMMaaS360SecurityEventCollector import get_events

    mocker.patch.object(client, "get_auth_token", return_value=AUTH_TOKEN)

    args = {"limit": limit}

    events, _results = get_events(
        client=client,
        args=args,
    )

    expected_admin_changes = [event for page in admin_changes for event in page["adminChanges"]["adminChange"]][:limit]
    expected_login_len = limit - len(expected_admin_changes)
    expected_login_reports = [event for page in login_reports for event in page["loginEvents"]["loginEvent"]][:expected_login_len]

    assert admin_changes_request.call_count == ceil(len(expected_admin_changes) / PAGE_SIZE)
    assert login_reports_request.call_count == ceil(len(expected_login_reports) / PAGE_SIZE)
    assert len(events) == len(expected_admin_changes) + len(expected_login_reports) == limit

    for event in events:
        event_time = event.pop("_time")
        log_type = event.pop("source_log_type")
        if log_type == AuditEventType.ChangesAudit.source_log_type:
            assert event in expected_admin_changes
            event_ts = event[AuditEventType.ChangesAudit.ts_field] / 1000
        else:
            assert event in expected_login_reports
            event_ts = event[AuditEventType.LoginReports.ts_field] / 1000

        expected_time = datetime.fromtimestamp(event_ts, tz=tz.UTC).strftime(DATE_FORMAT)
        assert event_time == expected_time