HPEArubaCentralEventCollector
This is the Aruba Central event collector integration for Cortex XSIAM.
Analytics & SIEM · HPE Aruba Central
Details
| ID | HPEArubaCentralEventCollector |
|---|---|
| Provider | HPE |
| Category | Analytics & SIEM |
| From Version | 8.4.0 |
| Docker Image | demisto/python3:3.12.13.10404775 |
| Supported Modules | XSIAM |
README
This is the Aruba Central event collector integration for Cortex XSIAM.
Configure HPE Aruba Central Event Collector in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | The region-specific Base URL for the Aruba Central API Gateway. | True |
| Authentication Method | The authentication method to use. “Access Token”: paste the token JSON downloaded from the Aruba Central UI. “Basic Auth”: provide a Username, Password, and Customer ID. | False |
| Client ID | The unique identifier for your API application registered in Aruba Central. | True |
| Client Secret | The secret key associated with your Client ID for API authentication. | True |
| Access Token (JSON) | The full token JSON downloaded from the Aruba Central UI (click the “Download Token” button and paste it here). The integration reads the refresh_token from it and refreshes access tokens automatically with no username/password. Required when the Authentication Method is “Access Token”. |
False |
| Customer ID | The unique identifier for your Aruba Central account. Required only when the Authentication Method is “Basic Auth”. | False |
| Username | The username of an Aruba Central account with at least read-only privileges. Required only when the Authentication Method is “Basic Auth”. | False |
| Password | The password associated with the specified Aruba Central username. Required only when the Authentication Method is “Basic Auth”. | False |
| Fetch Events | Select this to enable fetching events into Cortex. | False |
| Events Fetch Interval | The interval, in minutes, between event fetches. | False |
| Fetch networking events | Select this to fetch networking events in addition to audit logs. If cleared, the collector will only fetch audit logs. | False |
| The maximum number of audit events per fetch | The maximum number of audit events to pull in a single fetch. The default is 100. |
False |
| The maximum number of networking events per fetch | The maximum number of networking events to pull in a single fetch. The default is 5000. |
False |
| Trust any certificate (not secure) | Select this to bypass certificate validation. Use this only for testing or in trusted, isolated environments. | False |
How to Find Required Parameters
You can find most of the required API credentials within your HPE Aruba Central account.
- Log in to your Aruba Central account.
- Navigate to the Global Settings menu (or the equivalent management scope).
- Select API Gateway.
From this section, you can retrieve the following information:
- Access Token URL: Found on the APIs tab.
- Customer ID: Found on the APIs tab.
- Server URL: This is the base domain of your Aruba Central portal (e.g.,
https://app-uswest4.central.arubanetworks.com). - Client ID & Client Secret: Found on the My Apps tab. Select the application you created for XSOAR to view its details.
User Credentials:
- Username & Password: These are the credentials for the Aruba Central user account that you used to generate the API application (Client ID and Secret). This account must have at least read-only privileges.
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.
aruba-central-get-events
Gets events from Aruba Central.
Base Command
aruba-central-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. Default is 3 hours prior. | Optional |
Command Example
!aruba-central-get-events limit=5 should_push_events=false from_date='09-15-2024'
Context Output
There is no context output for this command.
Human Readable Output
Audit Events
cid classification cname description device_type gid has_details id ip_addr msp_id target ts user 50b8aef1ec00000000004ec069d890c4 Configuration Financial, Inc. Swarm configuration sync successful iap 8 false audit_trail_2024_9,AZHzPN000000c5NiQdg- 0.0.0.0 STANDALONE PHL000000K 1726362738 System 50b8aef1ec00000000004ec069d890c4 Configuration Financial, Inc. Swarm configuration sync successful iap 8 false audit_trail_2024_9,AZHzPR000000ByoC37es 0.0.0.0 STANDALONE PHL000000K 1726362751 System 50b8aef1ec00000000004ec069d890c4 Configuration Financial, Inc. Point configuration sync successful iap 28 false audit_trail_2024_9,AZH0C000000vzoci09Qm 0.0.0.0 STANDALONE CN00000CHM 1726376250 System 50b8aef1ec00000000004ec069d890c4 Configuration Financial, Inc. Swarm configuration sync successful iap 69 false audit_trail_2024_9,AZH00000000nKZfijDJe 0.0.0.0 STANDALONE CN00000HWY 1726389270 System 50b8aef1ec00000000004ec069d890c4 Configuration Financial, Inc. Swarm configuration sync successful iap 69 false audit_trail_2024_9,AZH16000000zKyye7zCq 0.0.0.0 STANDALONE PH000001TR 1726407685 System
Networking Events
bssid client_mac description device_mac device_serial device_type event_type event_uuid group_name has_rowdetail hostname labels level number sites timestamp ports: port 46 is now off-line 64:e8:00:00:5a:80 TW00K000HW SWITCH Ports 59b61831-7c71-1234-acf3-6f7c65d38fd7 003 - 2021 Standard NAC false IDFNT-000N01-ANSW02P3 {‘id’: 220, ‘name’: ‘Pac’} INFO 77 {‘id’: 37, ‘name’: ‘ID FNT Meridian 01 - 220618’} 1726358400000 ports: ST1-CMDR: port 1/42 is now off-line d4:e0:00:00:58:80 SG000YZ00V SWITCH Ports e1c8c68c-0eea-1234-a296-a77e67b9bf37 CT - 10 S LaSalle false ILCTT-C00001-ANSW31P2 {‘id’: 221, ‘name’: ‘Mid’} INFO 77 {‘id’: 25, ‘name’: ‘IL 01 - 904’} 1726358400000 There are no RADIUS servers configured. 64:e8:00:00:37:00 SG00J002CL SWITCH RADIUS 0b72cc8c-7ddb-1234-99a6-6669b3cf2a31 003 - 2021 Standard NAC false NEFNT-O00001-ANSW03P2 {‘id’: 125, ‘name’: ‘Corp_IT_Operations’} Informational 434 {‘id’: 18, ‘name’: ‘NE FNTG 01 - 3008’} 1726358400000 Mac Authentication failed for client b0:5c:da:9f:00:00 against server , 0.0.0.0. Failure reason: Missing Radius Server configuration 64:e8:00:00:37:00 SG00JQ000L SWITCH 450000c6-1234-4000-9c88-a162979ea016 003 - 2021 Standard NAC false NEFNT-O00001-ANSW03P2 {‘id’: 125, ‘name’: ‘Corp_IT_Operations’} Minor 43025 {‘id’: 158, ‘name’: ‘NE FNTG 01 - 300820’} 1726358400000 There are no RADIUS servers configured. 64:e8:00:00:37:00 SG00JQ000L SWITCH RADIUS 9bba8889-5aee-1234-808e-dda306e108b7 003 - 2021 Standard NAC false NEFNT-O00001-ANSW03P2 {‘id’: 125, ‘name’: ‘Corp_IT_Operations’} Informational 436 {‘id’: 15, ‘name’: ‘NE FNTG 01 - 3020’} 1726358401000
aruba-auth-test
Use this command to test the connectivity of the HPE Aruba Central instance.
Base Command
aruba-auth-test
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
Troubleshooting
Token expiration (Access Token method)
Access tokens are valid for 2 hours, and refresh tokens are valid for 15 days. If an access token is not renewed for 15 days (meaning the refresh token is unused for 15 days), Aruba Central removes the token. At this point, a new token must be generated either by going to the API Gateway UI (clicking the “Download Token” button and pasting the new token JSON here) or by using the OAuth API (Basic Auth method).
Configuration parameters
url— Server URL (required)auth_method— Authentication Methodcredentials— Client ID (required)token—customer_id—user— UsernameisFetchEvents— Fetch EventseventFetchInterval— Events Fetch Intervalfetch_networking_events— Fetch networking eventsmax_audit_events_per_fetch— The maximum number of audit events per fetchmax_networking_events_per_fetch— The maximum number of networking events per fetchinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (2)
-
aruba-auth-testUse this command to test the connectivity of the HPE Aruba Central instance.
-
aruba-central-get-eventsGets events from Aruba Central.
from CommonServerPython import DemistoException from HPEArubaCentralEventCollector import ( main, Client, NUM_OF_RETRIES, RATE_LIMIT_STATUS_CODE, BACKOFF_FACTOR, validate_authentication_params, mask_secret, ) import pytest import demistomock as demisto from CommonServerPython import date_to_timestamp from freezegun import freeze_time import json VENDOR = "aruba" PRODUCT = "central" DATE_FORMAT = "%Y-%m-%dT%H:%M:%S" BASE_URL = "https://server_url" CLIENT_ID = "test client id" CLIENT_SECRET = "test client secret" USER_NAME = "test username" USER_PASSWORD = "test password" CUSTOMER_ID = "test customer id" TEST_TOKEN = "testaccesstoken" TEST_REFRESH_TOKEN = "testrefreshtoken" FROM_DATE = "2024-09-11T03:21:33" FROM_TIME = int(date_to_timestamp(FROM_DATE, DATE_FORMAT) / 1000) CSRF_TOKEN = "testcsrftoken" SESSION_ID = "testsessionid" AUTH_CODE = "testauthcode" FETCH_DATE = "2024-09-12T03:21:33" FETCH_TIME = int(date_to_timestamp(FETCH_DATE, DATE_FORMAT) / 1000) FETCH_LIMIT = 10 # An Access Token bundle that contains only a refresh token (no usable access token), # so seeding it forces a refresh call. DOWNLOAD_TOKEN_REFRESH_ONLY = json.dumps({"refresh_token": TEST_REFRESH_TOKEN, "token_type": "bearer"}) # A full Access Token bundle that also includes a usable access token. DOWNLOAD_TOKEN_FULL = json.dumps( {"refresh_token": TEST_REFRESH_TOKEN, "access_token": TEST_TOKEN, "expires_in": 7200, "token_type": "bearer"} ) def util_load_json(path): with open(path, encoding="utf-8") as f: return json.loads(f.read()) def mock_instance_params(mocker, fetch_networking: bool = False): """ Helper function to mock the instance parameters. Args: mocker: pytest mocker object fetch_networking (bool): Instance fetch_networking parameter """ mocker.patch.object( demisto, "params", return_value={ "url": BASE_URL, "auth_method": "Basic Auth", "credentials": { "identifier": CLIENT_ID, "password": CLIENT_SECRET, }, "user": { "identifier": USER_NAME, "password": USER_PASSWORD, }, "customer_id": { "password": CUSTOMER_ID, }, "fetch_networking_events": fetch_networking, "max_audit_events_per_fetch": FETCH_LIMIT, "max_networking_events_per_fetch": FETCH_LIMIT, "proxy": False, "verify": False, }, ) @freeze_time(FETCH_DATE) @pytest.mark.parametrize("fetch_networking", [True, False]) def test_fetch_events_command(mocker, requests_mock, fetch_networking): """ Given: - Instance params When: - Running fetch-events command Then: - Ensure events are fetched and sent to XSIAM as expected """ audit_response_mock = util_load_json("test_data/mock_audit_response.json") networking_response_mock = util_load_json("test_data/mock_networking_response.json") requests_mock.get( f"{BASE_URL}/auditlogs/v1/events", request_headers={"authorization": f"Bearer {TEST_TOKEN}"}, json=audit_response_mock ) requests_mock.get( f"{BASE_URL}/monitoring/v2/events", request_headers={"authorization": f"Bearer {TEST_TOKEN}"}, json=networking_response_mock, ) mocker.patch.object(demisto, "command", return_value="fetch-events") mocker.patch.object( demisto, "getLastRun", return_value={ "last_audit_ts": FROM_TIME, "last_networking_ts": FROM_TIME, }, ) mock_instance_params(mocker, fetch_networking=fetch_networking) mocker.patch( "HPEArubaCentralEventCollector.get_integration_context", return_value={"access_token": TEST_TOKEN, "expiry_time": FETCH_TIME + 1}, ) send_events_to_xsiam_mock = mocker.patch("HPEArubaCentralEventCollector.send_events_to_xsiam", return_value={}) main() audit_response_mock["events"].reverse() expected_events = ( audit_response_mock["events"] if not fetch_networking else (audit_response_mock["events"] + networking_response_mock["events"]) ) send_events_to_xsiam_mock.assert_called_once_with(expected_events, vendor=VENDOR, product=PRODUCT) @freeze_time(FETCH_DATE) def test_get_access_token(mocker, requests_mock): """ Given: - A request to get an access token When: - No valid access token exists in the integration context Then: - Obtain a new access token by following the OAuth2 authorization code grant flow """ mocker.patch.object(demisto, "getIntegrationContext", return_value={}) # Mock login request def match_login_request(request): return request.qs.get("client_id")[0] == CLIENT_ID and request.json() == { "username": USER_NAME, "password": USER_PASSWORD, } requests_mock.post( f"{BASE_URL}/oauth2/authorize/central/api/login", additional_matcher=match_login_request, cookies={"csrftoken": CSRF_TOKEN, "session": SESSION_ID}, ) # Mock auth code request def match_auth_code_request(request): return ( request.qs.get("client_id")[0] == CLIENT_ID and request.qs.get("response_type")[0] == "code" and request.json() == {"customer_id": CUSTOMER_ID} ) requests_mock.post( f"{BASE_URL}/oauth2/authorize/central/api", additional_matcher=match_auth_code_request, request_headers={ "Content-Type": "application/json", "Cookie": f"session={SESSION_ID}", "X-CSRF-TOKEN": CSRF_TOKEN, }, json={"auth_code": AUTH_CODE}, ) # Mock token request def match_token_request(request): return request.json() == { "client_id": CLIENT_ID, "client_secret": CLIENT_SECRET, "grant_type": "authorization_code", "code": AUTH_CODE, } requests_mock.post( f"{BASE_URL}/oauth2/token", additional_matcher=match_token_request, json={ "refresh_token": TEST_REFRESH_TOKEN, "token_type": "bearer", "access_token": TEST_TOKEN, "expires_in": 7200, }, ) mocked_set_integration_context = mocker.patch("HPEArubaCentralEventCollector.set_integration_context") client = Client( base_url=BASE_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, user_name=USER_NAME, user_password=USER_PASSWORD, customer_id=CUSTOMER_ID, ) assert client.get_access_token() == TEST_TOKEN mocked_set_integration_context.assert_called_once_with( { "access_token": TEST_TOKEN, "expiry_time": FETCH_TIME + 7200, "refresh_token": TEST_REFRESH_TOKEN, } ) @freeze_time(FETCH_DATE) def test_refresh_access_token(mocker, requests_mock): """ Given: - A request to get an access token When: - There is an expired access token in the context Then: - Refresh the access token using the refresh token """ # Return an expired token from the context mocker.patch( "HPEArubaCentralEventCollector.get_integration_context", return_value={ "access_token": TEST_TOKEN, "expiry_time": FETCH_TIME - 1, "refresh_token": TEST_REFRESH_TOKEN, }, ) # Mock refresh request new_token = f"refreshed_{TEST_TOKEN}" new_refresh_token = f"refreshed_{TEST_REFRESH_TOKEN}" def match_refresh_request(request): expected_params = { "client_id": [CLIENT_ID], "client_secret": [CLIENT_SECRET], "grant_type": ["refresh_token"], "refresh_token": [TEST_REFRESH_TOKEN], } return request.qs == expected_params requests_mock.post( f"{BASE_URL}/oauth2/token", additional_matcher=match_refresh_request, json={ "refresh_token": new_refresh_token, "token_type": "bearer", "access_token": new_token, "expires_in": 7200, }, ) mocked_set_integration_context = mocker.patch("HPEArubaCentralEventCollector.set_integration_context") client = Client( base_url=BASE_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, user_name=USER_NAME, user_password=USER_PASSWORD, customer_id=CUSTOMER_ID, ) assert client.get_access_token() == new_token mocked_set_integration_context.assert_called_once_with( { "access_token": new_token, "expiry_time": FETCH_TIME + 7200, "refresh_token": new_refresh_token, } ) @freeze_time(FETCH_DATE) def test_fetch_with_duplicates(mocker, requests_mock): """ Given: - fetch events command When: - Some events with the same starting timestamp were previously fetched Then: - Fetched events are returned without the previously fetched ones """ fetch_networking = True full_audit_response = util_load_json("test_data/mock_audit_response.json") full_networking_response = util_load_json("test_data/mock_networking_response.json") mocker.patch.object(demisto, "command", return_value="fetch-events") mock_instance_params(mocker, fetch_networking=fetch_networking) mocker.patch( "HPEArubaCentralEventCollector.get_integration_context", return_value={"access_token": TEST_TOKEN, "expiry_time": FETCH_TIME + 1}, ) # Mock first fetch to get some of the events first_audit_response = full_audit_response.copy() first_audit_response["events"] = first_audit_response["events"][-2:] first_networking_response = full_networking_response.copy() first_networking_response["events"] = first_networking_response["events"][:2] requests_mock.get( f"{BASE_URL}/auditlogs/v1/events", request_headers={"authorization": f"Bearer {TEST_TOKEN}"}, json=first_audit_response ) requests_mock.get( f"{BASE_URL}/monitoring/v2/events", request_headers={"authorization": f"Bearer {TEST_TOKEN}"}, json=first_networking_response, ) mocker.patch.object(demisto, "getLastRun", return_value={}) set_last_run_mock = mocker.patch.object(demisto, "setLastRun") send_events_to_xsiam_mock = mocker.patch("HPEArubaCentralEventCollector.send_events_to_xsiam", return_value={}) main() expected_audit_events = list(reversed(first_audit_response["events"])) expected_networking_events = first_networking_response["events"] send_events_to_xsiam_mock.assert_called_once_with( expected_audit_events + expected_networking_events, vendor=VENDOR, product=PRODUCT ) # Mock next fetch to get all of the events, including the previously fetched requests_mock.get( f"{BASE_URL}/auditlogs/v1/events", request_headers={"authorization": f"Bearer {TEST_TOKEN}"}, json=full_audit_response ) requests_mock.get( f"{BASE_URL}/monitoring/v2/events", request_headers={"authorization": f"Bearer {TEST_TOKEN}"}, json=full_networking_response, ) mocker.patch.object(demisto, "getLastRun", return_value=set_last_run_mock.call_args[0][0]) send_events_to_xsiam_mock = mocker.patch("HPEArubaCentralEventCollector.send_events_to_xsiam", return_value={}) main() expected_audit_events = list(reversed(full_audit_response["events"][:-2])) expected_networking_events = full_networking_response["events"][2:] send_events_to_xsiam_mock.assert_called_once_with( expected_audit_events + expected_networking_events, vendor=VENDOR, product=PRODUCT ) @pytest.mark.parametrize("should_fail", [True, False]) def test_aruba_auth_test(mocker, should_fail): """ Given: - aruba-auth-test command When: - executing the 'aruba-auth-test' command Then: - CommnadResults will be returned with an informative message of success or failure if it's a known failure, otherwise an exception will be raised. """ mocker.patch.object(demisto, "command", return_value="aruba-auth-test") mock_instance_params(mocker) if should_fail: mocker.patch( "HPEArubaCentralEventCollector.fetch_events", side_effect=Exception("401 - Unauthorized access, authentication required"), ) else: mocker.patch("HPEArubaCentralEventCollector.fetch_events", return_value=({}, [], [])) if should_fail: return_error_mock = mocker.patch("HPEArubaCentralEventCollector.return_error") main() return_error_mock.assert_called() else: return_results_mock = mocker.patch("HPEArubaCentralEventCollector.return_results") main() assert return_results_mock.call_args[0][0].readable_output == "Authentication was successful." def test_test_module_full_bundle_is_non_mutating(mocker): """Test with a full bundle validates using the bundle's access token and never refreshes (non-mutating).""" from HPEArubaCentralEventCollector import test_module client = Client( base_url=BASE_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, user_name="", user_password="", customer_id="", access_token_bundle=DOWNLOAD_TOKEN_FULL, verify=False, proxy=False, ) validate = mocker.patch.object(client, "validate_access_token") get_token = mocker.patch.object(client, "get_access_token") assert test_module(client) == "ok" validate.assert_called_once_with(TEST_TOKEN) get_token.assert_not_called() def test_test_module_full_bundle_stale_access_token_raises_helpful_error(mocker): """ Given: - A full Access Token bundle whose access token is expired/stale (validation fails). When: - Pressing the Test button. Then: - The refresh token is NOT rotated (get_access_token is not called), and a DemistoException is raised that hints the token may be stale and can be refreshed via the 'aruba-auth-test' command. """ from HPEArubaCentralEventCollector import test_module client = Client( base_url=BASE_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, user_name="", user_password="", customer_id="", access_token_bundle=DOWNLOAD_TOKEN_FULL, verify=False, proxy=False, ) validate = mocker.patch.object( client, "validate_access_token", side_effect=DemistoException("Error in API call [401] - Unauthorized") ) get_token = mocker.patch.object(client, "get_access_token") with pytest.raises(DemistoException, match="aruba-auth-test"): test_module(client) validate.assert_called_once_with(TEST_TOKEN) # The refresh token must not be rotated by test-module. get_token.assert_not_called() def test_test_module_refresh_only_bundle_raises_without_refreshing(mocker): """ Given: - A bundle that contains only a refresh token (no usable access token). When: - Pressing the Test button. Then: - The refresh token is NOT rotated (get_access_token and validate_access_token are not called), and a DemistoException directs the user to the 'aruba-auth-test' command, because refreshing here would rotate a token that test-module cannot persist. """ from HPEArubaCentralEventCollector import test_module client = Client( base_url=BASE_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, user_name="", user_password="", customer_id="", access_token_bundle=DOWNLOAD_TOKEN_REFRESH_ONLY, verify=False, proxy=False, ) get_token = mocker.patch.object(client, "get_access_token") validate = mocker.patch.object(client, "validate_access_token") with pytest.raises(DemistoException, match="aruba-auth-test"): test_module(client) # Nothing that could rotate or use the refresh token should have been called. get_token.assert_not_called() validate.assert_not_called() def test_test_module_userpass_only_raises(): """ Given: - test-module command and a client configured with only Username/Password (no Access Token). When: - Pressing the Test button. Then: - A DemistoException is raised directing the user to the aruba-auth-test command, because a full OAuth login would burn Aruba's 30-minute new-token quota. """ from HPEArubaCentralEventCollector import test_module client = Client( base_url=BASE_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, user_name=USER_NAME, user_password=USER_PASSWORD, customer_id=CUSTOMER_ID, access_token_bundle="", verify=False, proxy=False, ) with pytest.raises(DemistoException, match="aruba-auth-test"): test_module(client) @freeze_time(FETCH_DATE) @pytest.mark.parametrize("fetch_networking, should_push_events", [(True, True), (False, False), (True, False), (False, True)]) def test_get_events_command(mocker, requests_mock, fetch_networking, should_push_events): """ Given: - Instance params and command args When: - Running the get events command Then: - Events are fetched and HR is returned. Events are pushed to XSIAM if should_push_events is true. """ audit_response_mock = util_load_json("test_data/mock_audit_response.json") networking_response_mock = util_load_json("test_data/mock_networking_response.json") requests_mock.get( f"{BASE_URL}/auditlogs/v1/events", request_headers={"authorization": f"Bearer {TEST_TOKEN}"}, json=audit_response_mock ) requests_mock.get( f"{BASE_URL}/monitoring/v2/events", request_headers={"authorization": f"Bearer {TEST_TOKEN}"}, json=networking_response_mock, ) mocker.patch.object(demisto, "command", return_value="aruba-central-get-events") mock_instance_params(mocker, fetch_networking=fetch_networking) mocker.patch.object( demisto, "args", return_value={ "should_push_events": should_push_events, "limit": FETCH_LIMIT, "from_date": FROM_DATE, }, ) mocker.patch( "HPEArubaCentralEventCollector.get_integration_context", return_value={"access_token": TEST_TOKEN, "expiry_time": FETCH_TIME + 1}, ) send_events_to_xsiam_mock = mocker.patch("HPEArubaCentralEventCollector.send_events_to_xsiam", return_value={}) return_results_mock = mocker.patch("HPEArubaCentralEventCollector.return_results") main() return_results_mock.assert_called() if fetch_networking: assert len(return_results_mock.call_args.args[0]) == 2 else: assert len(return_results_mock.call_args.args[0]) == 1 if should_push_events: audit_response_mock["events"].reverse() expected_events = ( audit_response_mock["events"] if not fetch_networking else (audit_response_mock["events"] + networking_response_mock["events"]) ) send_events_to_xsiam_mock.assert_called_once_with(expected_events, vendor=VENDOR, product=PRODUCT) else: send_events_to_xsiam_mock.assert_not_called() def _make_client() -> Client: """Helper to create a Client instance for http_request tests.""" return Client( base_url=BASE_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, user_name=USER_NAME, user_password=USER_PASSWORD, customer_id=CUSTOMER_ID, verify=False, proxy=False, ) def test_validate_access_token_uses_given_token_without_refresh(requests_mock): """validate_access_token calls the API with the given token as-is and does not refresh/rotate it.""" client = _make_client() audit = requests_mock.get(f"{BASE_URL}/auditlogs/v1/events", json={"events": [], "total": 0}) client.validate_access_token("some-access-token") assert audit.call_count == 1 assert audit.last_request.headers["authorization"] == "Bearer some-access-token" @freeze_time(FETCH_DATE) def test_http_request_retry_on_429_then_success(mocker): """ Given: - _http_request succeeds (simulating that the internal urllib3 retry handled a transient 429). When: - Calling client.http_request (which passes retry params to _http_request). Then: - The call succeeds and returns the expected data. - The retry-related params (retries, status_list_to_retry, backoff_factor) are passed to _http_request, proving the retry configuration is wired correctly. """ client = _make_client() mocker.patch.object(client, "get_access_token", return_value=TEST_TOKEN) expected_response = {"events": [{"id": 1}]} mock_http = mocker.patch.object( client, "_http_request", return_value=expected_response, ) result = client.http_request("GET", url_suffix="/test/endpoint", params={"limit": 10}) assert result == expected_response mock_http.assert_called_once_with( method="GET", url_suffix="/test/endpoint", params={"limit": 10}, headers={ "accept": "application/json", "authorization": f"Bearer {TEST_TOKEN}", }, retries=NUM_OF_RETRIES, status_list_to_retry=[RATE_LIMIT_STATUS_CODE], backoff_factor=BACKOFF_FACTOR, ) @freeze_time(FETCH_DATE) def test_http_request_repeated_429_raises_demisto_exception(mocker): """ Given: - _http_request always raises a DemistoException wrapping a 429 (rate-limit) response, exceeding NUM_OF_RETRIES attempts. When: - Calling client.http_request. Then: - A DemistoException is raised and is NOT absorbed by the "access token is invalid" branch. """ client = _make_client() mocker.patch.object(client, "get_access_token", return_value=TEST_TOKEN) mocker.patch.object( client, "_http_request", side_effect=DemistoException("Rate limit exceeded", res=mocker.MagicMock(status_code=429)), ) with pytest.raises(DemistoException, match="Rate limit exceeded"): client.http_request("GET", url_suffix="/test/endpoint") @freeze_time(FETCH_DATE) def test_http_request_passes_retry_params_on_initial_and_refresh(mocker): """ Given: - The first _http_request call raises a DemistoException containing "access token is invalid" (triggering the token-refresh branch). - The second _http_request call (after token refresh) succeeds. When: - Calling client.http_request. Then: - Both the initial and the post-refresh _http_request calls include retries=NUM_OF_RETRIES, status_list_to_retry=[RATE_LIMIT_STATUS_CODE], and backoff_factor=BACKOFF_FACTOR. """ import copy client = _make_client() refreshed_token = "refreshed_token" mocker.patch.object(client, "get_access_token", side_effect=[TEST_TOKEN, refreshed_token]) expected_response = {"events": [{"id": 2}]} # Capture deep copies of kwargs at call time, because http_request mutates the # headers dict in-place when refreshing the token, which would make both # call_args_list entries point to the same (mutated) dict object. captured_kwargs: list[dict] = [] original_side_effects = iter( [ DemistoException("access token is invalid"), expected_response, ] ) def capture_and_delegate(**kwargs): captured_kwargs.append(copy.deepcopy(kwargs)) result = next(original_side_effects) if isinstance(result, Exception): raise result return result mocker.patch.object(client, "_http_request", side_effect=capture_and_delegate) result = client.http_request("GET", url_suffix="/test/endpoint", params={"key": "val"}) assert result == expected_response assert len(captured_kwargs) == 2 expected_retry_kwargs = { "retries": NUM_OF_RETRIES, "status_list_to_retry": [RATE_LIMIT_STATUS_CODE], "backoff_factor": BACKOFF_FACTOR, } # Verify initial request (with original token) includes retry params first_kwargs = captured_kwargs[0] assert first_kwargs["headers"]["authorization"] == f"Bearer {TEST_TOKEN}" for key, value in expected_retry_kwargs.items(): assert first_kwargs[key] == value, f"Initial request missing or wrong retry param '{key}': {first_kwargs.get(key)}" # Verify post-refresh retry request (with refreshed token) includes retry params second_kwargs = captured_kwargs[1] assert second_kwargs["headers"]["authorization"] == f"Bearer {refreshed_token}" for key, value in expected_retry_kwargs.items(): assert second_kwargs[key] == value, f"Retry request missing or wrong retry param '{key}': {second_kwargs.get(key)}" @freeze_time(FETCH_DATE) def test_get_access_token_seeds_from_access_token_bundle(mocker, requests_mock): """ Given: - An instance configured with a pasted Access Token JSON (refresh token only) and no username/password. When: - get_access_token is called with an empty integration context. Then: - The refresh token from the bundle is used to obtain an access token, and the context is seeded with 'seeded_token' so it is not re-seeded on subsequent runs. """ mocker.patch("HPEArubaCentralEventCollector.get_integration_context", return_value={}) new_token = f"seeded_{TEST_TOKEN}" new_refresh_token = f"rotated_{TEST_REFRESH_TOKEN}" def match_refresh_request(request): return request.qs.get("refresh_token") == [TEST_REFRESH_TOKEN] requests_mock.post( f"{BASE_URL}/oauth2/token", additional_matcher=match_refresh_request, json={ "refresh_token": new_refresh_token, "token_type": "bearer", "access_token": new_token, "expires_in": 7200, }, ) mocked_set_integration_context = mocker.patch("HPEArubaCentralEventCollector.set_integration_context") client = Client( base_url=BASE_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, user_name="", user_password="", customer_id="", access_token_bundle=DOWNLOAD_TOKEN_REFRESH_ONLY, ) assert client.get_access_token() == new_token # The context is persisted twice: once when seeding, and again after the refresh. mocked_set_integration_context.assert_called_with( { "seeded_token": DOWNLOAD_TOKEN_REFRESH_ONLY, "access_token": new_token, "expiry_time": FETCH_TIME + 7200, "refresh_token": new_refresh_token, } ) @freeze_time(FETCH_DATE) def test_get_access_token_no_credentials_and_no_token_raises(mocker): """ Given: - An instance with no cached/refresh token, no downloaded token, and no username/password. When: - get_access_token is called. Then: - A DemistoException is raised instead of attempting a doomed login (prevents the 401/429 loop). """ mocker.patch("HPEArubaCentralEventCollector.get_integration_context", return_value={}) client = Client( base_url=BASE_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, user_name="", user_password="", customer_id="", access_token_bundle="", ) with pytest.raises(DemistoException, match="Unable to authenticate"): client.get_access_token() def test_validate_authentication_params_download_token_missing_token(): """ Given: - Authentication Method is "Access Token" but no Access Token is provided. When: - validate_authentication_params is called. Then: - A DemistoException is raised. """ with pytest.raises(DemistoException, match="no Access Token was provided"): validate_authentication_params("Access Token", "", "", "", "") def test_validate_authentication_params_userpass_missing_user(): """ Given: - Authentication Method is "Basic Auth" but Username/Password are missing. When: - validate_authentication_params is called. Then: - A DemistoException is raised. """ with pytest.raises(DemistoException, match="Username and/or Password is missing"): validate_authentication_params("Basic Auth", "", "", "", CUSTOMER_ID) def test_validate_authentication_params_userpass_missing_customer_id(): """ Given: - Authentication Method is "Basic Auth", Username/Password provided, but no Customer ID. When: - validate_authentication_params is called. Then: - A DemistoException is raised. """ with pytest.raises(DemistoException, match="Customer ID is missing"): validate_authentication_params("Basic Auth", "", USER_NAME, USER_PASSWORD, "") def test_validate_authentication_params_valid_configs(): """ Given: - Valid configurations: Access Token method with a token, or Basic Auth method with a Customer ID. When: - validate_authentication_params is called. Then: - No exception is raised. """ # Access Token method: only the token is needed. validate_authentication_params("Access Token", DOWNLOAD_TOKEN_REFRESH_ONLY, "", "", "") # Basic Auth method with a Customer ID. validate_authentication_params("Basic Auth", "", USER_NAME, USER_PASSWORD, CUSTOMER_ID) def test_parse_download_token_full_json(): """ Given: - A full Access Token JSON bundle (as downloaded from the Aruba Central UI). When: - Client.parse_download_token is called. Then: - The refresh_token, access_token, and expires_in are extracted. """ bundle = { "access_token": TEST_TOKEN, "refresh_token": TEST_REFRESH_TOKEN, "expires_in": 7200, "token_type": "bearer", "scope": "all", } refresh_token, access_token, expires_in = Client.parse_download_token(json.dumps(bundle)) assert refresh_token == TEST_REFRESH_TOKEN assert access_token == TEST_TOKEN assert expires_in == 7200 def test_parse_download_token_bare_string_rejected(): """ Given: - A bare refresh-token string instead of the full Access Token JSON. When: - Client.parse_download_token is called. Then: - A DemistoException is raised (the full JSON bundle is required). """ with pytest.raises(DemistoException, match="not valid JSON"): Client.parse_download_token(TEST_REFRESH_TOKEN) def test_parse_download_token_invalid_json(): """ Given: - A value that looks like JSON but is malformed. When: - Client.parse_download_token is called. Then: - A DemistoException is raised. """ with pytest.raises(DemistoException, match="not valid JSON"): Client.parse_download_token('{"refresh_token": ') def test_parse_download_token_json_missing_refresh_token(): """ Given: - A JSON bundle without a refresh_token field. When: - Client.parse_download_token is called. Then: - A DemistoException is raised. """ with pytest.raises(DemistoException, match="missing the required 'refresh_token' field"): Client.parse_download_token(json.dumps({"access_token": TEST_TOKEN, "expires_in": 7200})) @freeze_time(FETCH_DATE) def test_get_access_token_seeds_access_token_from_json_bundle(mocker): """ Given: - An Access Token pasted as a full JSON bundle that includes a still-valid access_token. When: - get_access_token is called with an empty integration context. Then: - The access_token from the bundle is used directly (no refresh call) and the context is seeded. """ mocker.patch("HPEArubaCentralEventCollector.get_integration_context", return_value={}) mocked_set_integration_context = mocker.patch("HPEArubaCentralEventCollector.set_integration_context") bundle = json.dumps( { "access_token": TEST_TOKEN, "refresh_token": TEST_REFRESH_TOKEN, "expires_in": 7200, "token_type": "bearer", } ) client = Client( base_url=BASE_URL, client_id=CLIENT_ID, client_secret=CLIENT_SECRET, user_name="", user_password="", customer_id="", access_token_bundle=bundle, ) # No requests_mock is set up: if the code tried to refresh, it would fail. It should use the seeded token. assert client.get_access_token() == TEST_TOKEN mocked_set_integration_context.assert_called_once_with( { "seeded_token": bundle, "refresh_token": TEST_REFRESH_TOKEN, "access_token": TEST_TOKEN, "expiry_time": FETCH_TIME + 7200, } ) @pytest.mark.parametrize( "secret, expected", [ (None, "<none>"), # never set ("", "<empty>"), # set but empty (distinct from None) ("short", "***(5)"), ("exactlytwelve", "exac…elve(13)"), # len 13 > 3*4 -> prefix + suffix shown ("abcdefghijkl", "***(12)"), # len 12 == 3*4 -> fully masked ("paf8bUQEpxcjxeFYfIlTiiO7fDvuZy4R", "paf8…Zy4R(32)"), # first 4 + last 4 + length ], ) def test_mask_secret(secret, expected): """ Given: secrets in every state (None, empty, short, and long). When: mask_secret is called. Then: None, empty, and short each produce a distinct, non-revealing marker, and long values expose only a short prefix+suffix plus length. """ result = mask_secret(secret) # The full secret must never appear in the masked output. if secret: assert secret not in result assert result == expected