VectraAIEventCollector
Collects Vectra Detections and Audits into XSIAM Events.
Network Security · Vectra AI
Details
| ID | VectraAIEventCollector |
|---|---|
| Provider | Vectra AI |
| Category | Network Security |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
Collects Vectra Detections and Audits into XSIAM Events.
This integration was integrated and tested with version 2.2 of Vectra.
This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.
Configure Vectra Event Collector in Cortex
| Parameter | Description | Required |
|---|---|---|
| Your server URL | True | |
| API Token | The API Token to use for authentication. | True |
| First fetch time | False | |
| Fetch Limit | Maximum amount of detections to fetch. Audits API does not include a fetch limit therefore this configuration is only relevant to detections. | False |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False |
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.
vectra-get-events
Fetches events (detections and audits) from Vectra.
Base Command
vectra-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. | Required |
Context Output
There is no context output for this command.
Configuration parameters
url— Your server URL (required)credentials— (required)first_fetch— First fetch timefetch_limit— Fetch Limitinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (1)
-
vectra-get-eventsFetches events (detections and audits) from Vectra.
""" Unit tests for Vectra Event Collector """ import json from datetime import datetime from pathlib import Path from typing import Any import pytest from CommonServerPython import * from freezegun import freeze_time from hypothesis import given, settings from hypothesis import strategies as st from pytest_mock import MockerFixture from VectraAIEventCollector import ( AUDIT_NEXT_RUN_KEY, AUDIT_START_TIMESTAMP_FORMAT, DETECTION_TIMESTAMP_KEY, DETECTION_TIMESTAMP_QUERY_FORMAT, XSIAM_TIME_FORMAT, VectraClient, add_parsing_rules, fetch_events, get_audits_cmd, get_audits_to_send, get_detections_cmd, get_events, get_most_recent_detection, module_test, ) """ Constants """ BASE_URL = "mock://dev.vectra.ai" PASSWORD = "9455w0rd" client = VectraClient(url=BASE_URL, api_key=PASSWORD) def load_json(path: Path): with open(path, encoding="utf-8") as f: return json.load(f) AUDITS: dict[str, Any] = load_json(Path("./test_data/audits.json")) DETECTIONS: dict[str, Any] = load_json(Path("./test_data/search_detections.json")) """ VectraClient Tests """ @settings(max_examples=10) @given(st.text()) def test_create_headers(token: str): """ Given: - A Vectra client. When: - A token is supplied. Then: - Authentication headers match. """ client = VectraClient("url.dev,", api_key=token) actual = client._create_headers() expected = {"Content-Type": "application/json", "Authorization": f"Token {token}"} assert "Content-Type" in actual assert "Authorization" in actual assert actual == expected def test_module_test(mocker: MockerFixture): """ Given - A dictionary of endpoints When - Calling ``test-module``. Then - Make sure that result succeeds. """ mocker.patch.object(client, "get_audits", return_value=AUDITS) mocker.patch.object(client, "get_detections", return_value=DETECTIONS) mocker.patch.object(demisto, "getLastRun", return_value={}) actual = module_test(client) assert "ok" in actual def test_module_test_exception(mocker: MockerFixture): """ Given - A dictionary of endpoints When - Calling ``test-module`` with an ``Exception`` side effect. Then - Make sure that result fails. """ mocker.patch.object( client, "_http_request", side_effect=Exception("test module failed"), ) mocker.patch.object(demisto, "getLastRun", return_value={}) with pytest.raises(Exception) as e: module_test(client) assert "test module failed" in str(e.value) def test_get_detections(mocker: MockerFixture): """ Test Vectra client `get_detections` method. """ mocker.patch.object(client, "_http_request", return_value=DETECTIONS) response: dict[str, Any] = client.get_detections(1) assert isinstance(response, dict) def test_get_audits(mocker: MockerFixture): """ Test Vectra client `get_audits` method. """ start = datetime.now().strftime(AUDIT_START_TIMESTAMP_FORMAT) mocker.patch.object(client, "_http_request", return_value=AUDITS) response: dict[str, Any] = client.get_audits(start) assert isinstance(response, dict) assert not client.max_fetch < len(response.get("audits")) """ Command Tests """ @pytest.mark.parametrize( "detections,audits", [(DETECTIONS, AUDITS), ({}, {}), (DETECTIONS, {}), ({}, AUDITS)], ) class TestCommands: def test_get_detections_cmd(self, mocker: MockerFixture, detections: dict[str, Any], audits: dict[str, Any]): """ Test `vectra-get-events` method detections part. """ mocker.patch.object(client, "get_detections", return_value=detections) cmd_res = get_detections_cmd(client, first_timestamp="") if detections: assert len(cmd_res.outputs) == len(detections.get("results")) else: assert "No detections found" in cmd_res.readable_output def test_get_audits_cmd(self, mocker: MockerFixture, detections: dict[str, Any], audits: dict[str, Any]): """ Test `vectra-get-events` method audits part. """ mocker.patch.object(client, "get_audits", return_value=audits) cmd_res = get_audits_cmd(client, "1970-01-01 00:00:00") if audits: assert len(cmd_res.outputs) == len(audits.get("audits")) else: assert "No audits found" in cmd_res.readable_output def test_get_events(self, mocker: MockerFixture, detections: dict[str, Any], audits: dict[str, Any]): """ Test the `vectra-get-events` command. Given: - Detections and Audits raw responses. When: - Case A: Both detections and audits are returned. - Case B: Both detections and audits are empty. - Case C: Detections are returned, audits is empty. - Case D: Audts are returned, detections is empty. Then: - The `CommandResults::outputs` of detections are equal to the ones raw response. - The `CommandResults::outputs` of audits are equal to the ones raw response. """ mocker.patch.object(client, "get_detections", return_value=detections) mocker.patch.object(client, "get_audits", return_value=audits) detection_res, audits_res = get_events(client, datetime.now()) assert detection_res.outputs == detections.get("results") assert audits_res.outputs == audits.get("audits") @freeze_time("1970-01-01 00:00:00") def test_first_fetch( self, mocker: MockerFixture, detections: dict[str, Any], audits: dict[str, Any], ): """ Given: - Fetching for the first time, first_fetch set to default (3 days) When: - Case A: Detections and Audits were fetched. - Case B: No Detections nor Audits were fetched. - Case C: Detections were fetched, Audits were not fetched. - Case D: Detections were not fetched, Audits were fetched. Then: - Case A: Detections next fetch will be set to now + 1 minute, audits next fetch will be set to today. - Case B: Detections next fetch will be set to last fetched detections, audits next fetch will be set to today. - Case C: Same as Case A. - Case D: Same as Case B. """ mocker.patch.object(client, "get_detections", return_value=detections) mocker.patch.object(client, "get_audits", return_value=audits) mocker.patch.object(demisto, "getLastRun", return_value={}) detections_actual, audits_actual, next_fetch = fetch_events(client) if audits_actual: assert len(audits_actual) == 5 assert next_fetch.get(AUDIT_NEXT_RUN_KEY) == AUDITS.get("audits")[-1].get("vectra_timestamp") else: assert next_fetch.get(AUDIT_NEXT_RUN_KEY) == "0" if detections_actual: assert len(detections_actual) == 5 assert next_fetch.get(DETECTION_TIMESTAMP_KEY) == "2022-09-14T0105" else: assert next_fetch.get(DETECTION_TIMESTAMP_KEY) == "1969-12-29T0000" @freeze_time("2023-02-19 00:00:13") def test_not_first_fetch(self, mocker: MockerFixture, detections: dict[str, Any], audits: dict[str, Any]): """ Given: - Not the first fetch. - The frozen date is timestamp 1676764813, the 2nd audit from the list. When: - Case A: Detections and 3 Audits were fetched. - Case B: No Detections nor Audits were fetched. - Case C: Detections were fetched, Audits were not fetched. - Case D: Detections were not fetched, 3 Audits were fetched. Then: - Case A: Detections next fetch will be set to now + 1 minute, audits next fetch will to last audit timestamp. - Case B: Detections next fetch will be set to last fetched detections, audits next fetch will be set to last audit timestamp. - Case C: Same as Case A. - Case D: Same as Case B. """ mocker.patch.object(client, "get_detections", return_value=detections) mocker.patch.object(client, "get_audits", return_value=audits) mocker.patch.object( demisto, "getLastRun", return_value={ DETECTION_TIMESTAMP_KEY: datetime.now().strftime(DETECTION_TIMESTAMP_QUERY_FORMAT), AUDIT_NEXT_RUN_KEY: str(datetime.now().timestamp()), }, ) detections_actual, audits_actual, next_fetch = fetch_events(client) if audits_actual: assert len(audits_actual) == 3 assert next_fetch.get(AUDIT_NEXT_RUN_KEY) == AUDITS.get("audits")[-1].get("vectra_timestamp") if detections_actual: assert next_fetch.get(DETECTION_TIMESTAMP_KEY) == "2022-09-14T0105" else: assert next_fetch.get(DETECTION_TIMESTAMP_KEY) == "2023-02-19T0000" """ Helper Functions Tests """ @pytest.mark.parametrize( "audits", [(AUDITS.get("audits")), ([])], ) def test_get_audits_to_send_first_fetch(audits: List[dict[str, Any]]): """ Given: audits returned from the endpoint. When: - Case A: 4 Audits returned. - Case B: No audits returned. Then: - Case A: 4 audits returned. - Case B: No audits returned. """ actual = get_audits_to_send(audits, True, None) assert actual == audits @pytest.mark.parametrize( "audits,prev_fetch_ts_str,expected", [ (AUDITS.get("audits"), "0", AUDITS.get("audits")), (AUDITS.get("audits"), "1676764803", AUDITS.get("audits")[1:]), ([], "0", []), ([], "1676764803", []), ], ) def test_get_audits_to_send_not_first_fetch(audits: List[dict[str, Any]], prev_fetch_ts_str: str, expected: List[dict[str, Any]]): """ Given: audits returned from the endpoint and it's not a first fetch. When: - Case A: 4 Audits returned, . - Case B: No audits returned. Then: - Case A: 4 audits returned. - Case B: No audits returned. """ actual = get_audits_to_send(audits, False, prev_fetch_ts_str) assert actual == expected @pytest.mark.parametrize( "event,expected_time,format", [ (DETECTIONS.get("results")[0], "2022-09-01T02:15:48.000Z", XSIAM_TIME_FORMAT), (AUDITS.get("audits")[0], "2023-02-19T00:00:03.000Z", XSIAM_TIME_FORMAT), ], ) def test_add_parsing_rules(event: dict[str, Any], expected_time: str, format: str): """ Given: An Event. When: - Case A: The event is a detection. - Case B: The event is an audit. Then: - Case A/B: The event should have a property _time in format XSIAM_TIME_FORMAT. """ actual = add_parsing_rules(event) assert "_time" in actual assert actual["_time"] == expected_time assert datetime.strptime(actual["_time"], format) @pytest.mark.parametrize( "detections,expected", [ ( [ {DETECTION_TIMESTAMP_KEY: "2022-09-14T00:54:56Z"}, {DETECTION_TIMESTAMP_KEY: "2022-09-16T00:54:56Z"}, {DETECTION_TIMESTAMP_KEY: "2022-09-15T00:54:56Z"}, ], "2022-09-16T00:54:56Z", ), (DETECTIONS.get("results"), "2022-09-14T01:04:43Z"), ], ) def test_get_most_recent_detection(detections: List[dict[str, Any]], expected: str): """ Given: A list of detections When: - Case A: A list of adjacent timewise detections are provided. - Case B: A list of far off detections are provided. """ actual = get_most_recent_detection(detections=detections) assert isinstance(actual, dict) assert actual.get(DETECTION_TIMESTAMP_KEY) == expected