IBMMaaS360Security
This is the IBM MaaS360 Security event collector integration for Cortex XSIAM.
Analytics & SIEM · IBM MaaS360 Security
Details
| ID | IBMMaaS360Security |
|---|---|
| Provider | IBM |
| Category | Analytics & SIEM |
| From Version | 8.4.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | XSIAM |
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:
-
Log in to the IBM MaaS360 portal.
-
Go to Setup > Manage Access Keys.

-
Click Generate Access Key.

-
Create a new
MaaS360 Web Serviceskey.

-
Get App ID, App Version, Platform ID, and App 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
-
Log in to the IBM MaaS360 portal.
-
Hover over the profile menu on the top right and copy the Account ID value. This is your Billing ID.

Account permissions
To collect events, the user requires read-only permissions.
Follow these steps to modify user permissions:
-
Log in to the IBM MaaS360 portal.
-
Go to Setup > Administrators

-
Find the relevant administrator account or create a new one.
-
Click Edit. (Note: This does not appear for the primary admin account.)

-
Add the Read-Only permission.

Configure IBM MaaS360 Security on Cortex XSIAM
- Navigate to Settings > Configurations > Data Collection > Automations & Feed Integrations.
- Search for IBM MaaS360 Security.
-
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 - 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 fetchmax_admin_change_audits_per_fetch— Maximum number of Admin Changes Audit events per fetchinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (1)
-
ibm-maas360-security-get-eventsGets 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