CybelAngel Event Collector
CybelAngel collects reports from the CybelAngel platform, which specializes in external attack surface protection and management.
Data Enrichment & Threat Intelligence · CybelAngel
Details
| ID | CybelAngel Event Collector |
|---|---|
| Provider | CybelAngel |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 8.0.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
CybelAngel receives reports from the CybelAngel platform, which specializes in external attack surface protection and management
Configure CybelAngel in Cortex
Required Modules (For XSIAM)
-
Account Takeover Protection module is required for Credentials watchlist events.
-
Domain Protection module is required for Domain events.
| Parameter | Required |
|---|---|
| Server URL | True |
| Client ID | True |
| Client Secret | True |
| Trust any certificate (not secure) | False |
| Use system proxy settings | False |
| Fetch events | True |
| Event Types To Fetch | True |
| The maximum number of report event per fetch | False |
| The maximum number of Credential watchlist events per fetch | False |
| The maximum number of Domain watchlist events per fetch | 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.
cybelangel-get-events
Send events from CybelAngel to XSIAM. Used mainly for debugging.
Base Command
cybelangel-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| should_push_events | Set to True in order to create events, otherwise will only display them. Possible values are: True, False. Default is False. | Required |
| events_type_to_fetch | Which events to fetch. Possible values are: Reports, Domain wathclish, Credentials watchlist. Default is Reports. | Optional |
| start_date | Get events from a specific start date formatted with ISO 8601, Example: “2025-01-14T10:22:45”. | Optional |
| end_date | Get events until a specific end date formatted with ISO 8601, Example: “2025-01-14T10:22:45”. | Optional |
| limit | The maximum number of events to return. Default is 50. | Optional |
Context Output
There is no context output for this command.
cybelangel-report-status-update
Update the status of one or multiple reports.
Base Command
cybelangel-report-status-update
Input
| Argument Name | Description | Required |
|---|---|---|
| report_ids | List of report IDs to update. | Required |
| status | The new status of the reports. Possible values are: draft, open, in_progress, resolved, discarded. | Required |
Context Output
There is no context output for this command.
Command example
!cybelangel-report-status-update report_ids=1234 status=open
Human Readable Output
“The status of the following reports </report list> has been successfully updated to </report status>.”
cybelangel-report-get
Retrieve reports from CybelAngel.
Base Command
cybelangel-report-get
Input
| Argument Name | Description | Required |
|---|---|---|
| report_id | The ID of the report to retrieve. | Required |
| If true, retrieves the report as a PDF file. Possible values are: True, False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| CybelAngel.Report | unknown | The retrieved report. |
| InfoFile.EntryID | String | Entry ID of the saved PDF file. |
Command example
!cybelangel-report-get report_id=1234
Context Example
{
"CybelAngel": {
"Report": {
"abstract": "Example Output.",
"abuse_email": "",
"analysis": "Example Output.",
"asset_urls": [],
"attachments": [
{
"attached_to": "report_id",
"id": "1234",
"name": "Example Output.csv"
}
],
"board": "",
"category": "leak",
"city": "",
"country_code": "",
"created_at": "2000-11-26T13:25:16.116453",
"detected_at": "2000-11-26T10:45:05+00:00",
"domain_registered_at": null,
"hostnames": [],
"id": "1234",
"incident_id": "1234",
"incident_type": "Test",
"investigation_id": "1234:1234",
"ip": "",
"keywords": [
{
"id": "1234",
"name": "aa.net"
}
],
"liveness": {
"last_checked_at": "2000-11-26T13:25:15.716702+00:00",
"online": true
},
"location": "",
"machine_name": null,
"malware_location": null,
"malware_name": null,
"module": "account_Test",
"mx_servers": [],
"ns_servers": [],
"origins": [
{
"type": "malicious_actor",
"value": ""
}
],
"port": null,
"registrant_email": "",
"registrar_name": "",
"report_content": "Example Output.",
"report_type": "incident_detection",
"risks": [
{
"message": "Example Output.",
"type": "account_takeover"
},
{
"message": "Example Output.",
"type": "spear_phishing"
},
{
"message": "Example Output.",
"type": "social_engineering"
}
],
"samples": [
{
"sample": "See attachment",
"type": "other"
}
],
"screenshots": [],
"sender": "Example Output@cybelangel.com",
"sender_tenant_id": "cybelangel",
"sent_at": "20200-11-26T13:25:57+00:00",
"severity": 1,
"source": "Example platform",
"status": "resolved",
"stream": "1234",
"suggestions": [
{
"message": "Example Output.",
"type": "other"
}
],
"tags": [],
"threat": null,
"title": "Example Output. platform",
"updated_at": "2000-02-23T13:07:17.214040",
"url": "https://platform.cybelangel.com/reports/1234",
"user_session": null,
"usergroups": [
"Example Output.",
"TVMExample Output.SOC"
],
"volume": {
"bins": null,
"documents": null,
"domain": null,
"emails": 1,
"ips": null,
"passwords": 1
},
"whois": ""
}
}
}
Human Readable Output
Report ID example-id-6 details
| id | report_type | sender | severity | status | updated_at |
|---|---|---|---|---|---|
| example-id-6 | incident_detection | example@example.com | 1 | in_progress | 2025-03-03T09:13:33.253781 |
Command example
!cybelangel-report-get report_id=1234 pdf=true
Context Example
{
"InfoFile": {
"EntryID": "1234",
"Extension": "pdf",
"Info": "application/pdf",
"Name": "cybelangel_report_1234.pdf",
"Size": 127719,
"Type": "PDF document, version 1.4"
}
}
Human Readable Output
Returned file: cybelangel_report_1234.pdf
cybelangel-report-remediation-request-create
Create a remediation request for a report.
Base Command
cybelangel-report-remediation-request-create
Input
| Argument Name | Description | Required |
|---|---|---|
| report_id | The ID of the report. | Required |
| requestor_email | Email of the requestor. | Required |
| requestor_fullname | Full name of the requestor. | Required |
Context Output
There is no context output for this command.
Command example
!cybelangel-report-remediation-request-create report_id=1234 requestor_email=test@paloaltonetworks.com requestor_fullname="Example Test"
Context Example
{
"CybelAngel": {
"Report": {
"RemediationRequest": {
"report_id": "1234"
}
}
}
}
Human Readable Output
Remediation request was created for 1234.
cybelangel-report-attachment-get
Retrieve an attachment from a report.
Base Command
cybelangel-report-attachment-get
Input
| Argument Name | Description | Required |
|---|---|---|
| report_id | The ID of the report. | Required |
| attachment_id | The ID of the attachment. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.EntryID | unknown | Entry ID of the retrieved file. |
Command example
!cybelangel-report-attachment-get report_id=1234 attachment_id=5678
Context Example
{
"InfoFile": {
"EntryID": "1111",
"Extension": "csv",
"Info": "text/csv; charset=utf-8",
"Name": "cybelangel_report_1234_attachment_5678.csv",
"Size": 210,
"Type": "ASCII text"
}
}
Human Readable Output
cybelangel-archive-report-by-id-get
Retrieve an archived report by ID as a ZIP file.
Base Command
cybelangel-archive-report-by-id-get
Input
| Argument Name | Description | Required |
|---|---|---|
| report_id | The ID of the archived report. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| InfoFile.EntryID | unknown | Entry ID of the saved ZIP file. |
Command example
!cybelangel-archive-report-by-id-get report_id=1234
Context Example
{
"InfoFile": {
"EntryID": "1111",
"Extension": "zip",
"Info": "application/zip",
"Name": "cybelangel_archive_report_1234.zip",
"Size": 15604,
"Type": "Zip archive data, at least v2.0 to extract"
}
}
Human Readable Output
cybelangel-mirror-report-get
Retrieve the mirror details for the specified report.
Base Command
cybelangel-mirror-report-get
Input
| Argument Name | Description | Required |
|---|---|---|
| report_id | The ID of the report. | Required |
| csv | If true, retrieves the mirror report in CSV format. Possible values are: True, False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| CybelAngel.ReportMirror | unknown | Mirror details of the report. |
| InfoFile.EntryID | unknown | Entry ID of the saved CSV file. |
Command example
!cybelangel-mirror-report-get report_id=1234
Context Example
{
"CybelAngel": {
"ReportMirror": {
"available_files_count": 1,
"created_at": "2000-07-11T12:50:20Z",
"files_count": 1,
"files_volume": 6871,
"report_id": "1234",
"status": "expired",
"stream_id": "1234",
"updated_at": "2000-01-12T03:26:49Z"
}
}
}
Human Readable Output
Mirror details for Report ID example-id-7
| report_id | created_at | available_files_count | updated_at |
|---|---|---|---|
| example-id-7 | 2024-07-11T12:50:20Z | 1 | 2025-01-12T03:26:49Z |
Command example
!cybelangel-mirror-report-get report_id=1234 csv=true
Context Example
{
"InfoFile": {
"EntryID": "1111",
"Extension": "csv",
"Info": "text/csv; charset=utf-8",
"Name": "cybelangel_mirror_report_1234.csv",
"Size": 212,
"Type": "ASCII text"
}
}
Human Readable Output
cybelangel-report-comment-create
Create a new comment on a report.
Base Command
cybelangel-report-comment-create
Input
| Argument Name | Description | Required |
|---|---|---|
| discussion_id | The discussion_id is made of report id and tenant id like uuid:uuid. Example: [report_id]:[your-tenant-id]. | Required |
| content | The content of the comment. | Required |
| parent_id | The ID of the parent comment (for replies). | Optional |
| assigned | Specifies if the comment is assigned to analysts (true/false). | Optional |
Context Output
There is no context output for this command.
Command example
!cybelangel-report-comment-create report_id=1234 content="Test Comment"
Human Readable Output
Comment added to Report ID 1234.
cybelangel-report-list
Retrieve reports from CybelAngel.
Base Command
cybelangel-report-list
Input
| Argument Name | Description | Required |
|---|---|---|
| start_date | Get reports from a specific start date formatted with ISO 8601. | Optional |
| end_date | Get reports until a specific end date formatted with ISO 8601. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| CybelAngel.Report | unknown | The retrieved reports. |
Command example
!cybelangel-report-list start_date="19 hours ago" end_date="now"
Context Example
{
"CybelAngel": {
"Report": {
"reports": [
{
"abstract": "Example Output.",
"abuse_email": "",
"analysis": "Example Output.",
"asset_urls": [],
"attachments": [
{
"attached_to": "report_id",
"id": "1234",
"name": "Example Output.csv"
}
],
"board": "",
"category": "leak",
"city": "",
"country_code": "",
"created_at": "2000-11-26T13:25:16.116453",
"detected_at": "2000-11-26T10:45:05+00:00",
"domain_registered_at": null,
"hostnames": [],
"id": "1234",
"incident_id": "1234",
"incident_type": "Test",
"investigation_id": "1234:1234",
"ip": "",
"keywords": [
{
"id": "1234",
"name": "aa.net"
}
],
"liveness": {
"last_checked_at": "2000-11-26T13:25:15.716702+00:00",
"online": true
},
"location": "",
"machine_name": null,
"malware_location": null,
"malware_name": null,
"module": "account_Test",
"mx_servers": [],
"ns_servers": [],
"origins": [
{
"type": "malicious_actor",
"value": ""
}
],
"port": null,
"registrant_email": "",
"registrar_name": "",
"report_content": "Example Output.",
"report_type": "incident_detection",
"risks": [
{
"message": "Example Output.",
"type": "account_takeover"
},
{
"message": "Example Output.",
"type": "spear_phishing"
},
{
"message": "Example Output.",
"type": "social_engineering"
}
],
"samples": [
{
"sample": "See attachment",
"type": "other"
}
],
"screenshots": [],
"sender": "Example Output@cybelangel.com",
"sender_tenant_id": "cybelangel",
"sent_at": "20200-11-26T13:25:57+00:00",
"severity": 1,
"source": "Example platform",
"status": "resolved",
"stream": "1234",
"suggestions": [
{
"message": "Example Output.",
"type": "other"
}
],
"tags": [],
"threat": null,
"title": "Example Output. platform",
"updated_at": "2000-02-23T13:07:17.214040",
"url": "https://platform.cybelangel.com/reports/1234",
"user_session": null,
"usergroups": [
"Example Output.",
"TVMExample Output.SOC"
],
"volume": {
"bins": null,
"documents": null,
"domain": null,
"emails": 1,
"ips": null,
"passwords": 1
},
"whois": ""
},
{
"abstract": "Example Output.",
"abuse_email": "",
"analysis": "Example Output.",
"asset_urls": [],
"attachments": [
{
"attached_to": "report_id",
"id": "1234",
"name": "Example Output.csv"
}
],
"board": "",
"category": "leak",
"city": "",
"country_code": "",
"created_at": "2000-11-26T13:25:16.116453",
"detected_at": "2000-11-26T10:45:05+00:00",
"domain_registered_at": null,
"hostnames": [],
"id": "1234",
"incident_id": "1234",
"incident_type": "Test",
"investigation_id": "1234:1234",
"ip": "",
"keywords": [
{
"id": "1234",
"name": "aa.net"
}
],
"liveness": {
"last_checked_at": "2000-11-26T13:25:15.716702+00:00",
"online": true
},
"location": "",
"machine_name": null,
"malware_location": null,
"malware_name": null,
"module": "account_Test",
"mx_servers": [],
"ns_servers": [],
"origins": [
{
"type": "malicious_actor",
"value": ""
}
],
"port": null,
"registrant_email": "",
"registrar_name": "",
"report_content": "Example Output.",
"report_type": "incident_detection",
"risks": [
{
"message": "Example Output.",
"type": "account_takeover"
},
{
"message": "Example Output.",
"type": "spear_phishing"
},
{
"message": "Example Output.",
"type": "social_engineering"
}
],
"samples": [
{
"sample": "See attachment",
"type": "other"
}
],
"screenshots": [],
"sender": "Example Output@cybelangel.com",
"sender_tenant_id": "cybelangel",
"sent_at": "20200-11-26T13:25:57+00:00",
"severity": 1,
"source": "Example platform",
"status": "resolved",
"stream": "1234",
"suggestions": [
{
"message": "Example Output.",
"type": "other"
}
],
"tags": [],
"threat": null,
"title": "Example Output. platform",
"updated_at": "2000-02-23T13:07:17.214040",
"url": "https://platform.cybelangel.com/reports/1234",
"user_session": null,
"usergroups": [
"Example Output.",
"TVMExample Output.SOC"
],
"volume": {
"bins": null,
"documents": null,
"domain": null,
"emails": 1,
"ips": null,
"passwords": 1
},
"whois": ""
}
]
}
}
}
Human Readable Output
Reports list
| id | url | report_type | sender | severity | status | updated_at | report_content |
|---|---|---|---|---|---|---|---|
| example-id-1 | https://platform.example.com/reports/example-id-1 | incident_detection | example@example.com | 1 | open | 2025-02-25T13:06:06.821922 | ### Sample content… Example |
| example-id-2 | https://platform.example.com/reports/example-id-2 | incident_detection | example@example.com | 1 | resolved | 2025-02-26T18:58:50.303598 | ### Sample content… Example |
| example-id-3 | https://platform.example.com/reports/example-id-3 | incident_detection | example@example.com | 1 | in_progress | 2025-02-26T12:17:42.241832 | ### Sample content… Example |
| example-id-4 | https://platform.example.com/reports/example-id-4 | incident_detection | example@example.com | 1 | open | 2025-02-26T13:29:54.520708 | ### Sample content… Example |
| example-id-5 | https://platform.example.com/reports/example-id-5 | incident_detection | example@example.com | 2 | open | 2025-02-25T16:29:32.696281 | ### Sample content… Example |
| example-id-6 | https://platform.example.com/reports/example-id-6 | incident_detection | example@example.com | 1 | in_progress | 2025-03-03T09:13:33.253781 | ### Sample content… Example |
| example-id-7 | https://platform.example.com/reports/example-id-7 | incident_detection | example@example.com | 1 | in_progress | 2025-03-03T09:13:33.253781 | ### Sample content… Example |
| example-id-8 | https://platform.example.com/reports/example-id-8 | incident_detection | example@example.com | 1 | open | 2025-03-03T14:26:11.424002 | ### Sample content… Example |
| example-id-9 | https://platform.example.com/reports/example-id-9 | incident_detection | example@example.com | 1 | open | 2025-03-03T14:22:14.184243 | ### Sample content… Example |
| example-id-10 | https://platform.example.com/reports/example-id-10 | incident_detection | example@example.com | 1 | open | 2025-03-03T14:28:22.089922 | ### Sample content… Example |
cybelangel-report-comments-get
Retrieve comments from a report.
Base Command
cybelangel-report-comments-get
Input
| Argument Name | Description | Required |
|---|---|---|
| report_id | The ID of the report. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| CybelAngel.Report.Comment | unknown | The list of comments for the report. |
Command example
!cybelangel-report-comments-get report_id=1234
Context Example
{
"CybelAngel": {
"Report": {
"Comments": {
"comments": [
{
"assigned": false,
"author": {
"firstname": "Example",
"id": "1234",
"lastname": "Test"
},
"content": "Test Comment 2",
"created_at": "2000-07-11T15:29:05Z",
"discussion_id": "1234:5678",
"discussion_tenant_name": "Test",
"id": "1234",
"isNew": false,
"last_updated_at": "2000-07-11T15:29:05Z"
},
{
"assigned": false,
"author": {
"firstname": "Example",
"id": "1234",
"lastname": "Test"
},
"content": "Test Comment 2",
"created_at": "2000-07-11T15:29:05Z",
"discussion_id": "1234:5678",
"discussion_tenant_name": "Test",
"id": "1234",
"isNew": false,
"last_updated_at": "2000-07-11T15:29:05Z"
}
],
"new": 0,
"total": 2,
"id": "1234"
}
}
}
}
Human Readable Output
Comments for Report ID example-id-8
| content | created_at | discussion_id | assigned | author_firstname | author_lastname | last_updated_at |
|---|---|---|---|---|---|---|
| This is a comment message | 2025-02-27T11:04:05Z | example-id-8:example-tenant-id | false | ExampleFirst | ExampleLast | 2025-02-27T11:04:05Z |
<~PLATFORM>
License Requirements
The following configuration parameters require the Cortex XSIAM license:
- Fetch events
</~PLATFORM>
Configuration parameters
url— Server URL (required)credentials— Client ID (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetchEvents— Fetch eventseventFetchInterval— Events Fetch Intervalevent_types_to_fetch— Event Types To Fetchmax_fetch— The maximum number of reports per fetch (required)max_fetch_creds— The maximum number of Credential watchlist events per fetchmax_fetch_domain— The maximum number of Domain watchlist events per fetch
Commands (10)
-
cybelangel-archive-report-by-id-getRetrieve an archived report by ID as a ZIP file.
-
cybelangel-get-eventsSend events from CybelAngel to XSIAM. Used mainly for debugging.
-
cybelangel-mirror-report-getRetrieve the mirror details for the specified report.
-
cybelangel-report-attachment-getRetrieve an attachment from a report.
-
cybelangel-report-comment-createCreate a new comment on a report.
-
cybelangel-report-comments-getRetrieve comments from a report.
-
cybelangel-report-getRetrieve reports from CybelAngel.
-
cybelangel-report-listRetrieve reports from CybelAngel.
-
cybelangel-report-remediation-request-createCreate a remediation request for a report.
-
cybelangel-report-status-updateUpdate the status of one or multiple reports.
import random from unittest.mock import MagicMock from CybelAngelEventCollector import Client, DATE_FORMAT, REPORT, DOMAIN, CREDENTIALS, LATEST_TIME, LATEST_FETCHED_IDS, EVENT_TYPE from CommonServerPython import * import pytest TEST_URL = "https://test.com/api" DEFAULT_PARAMS = { "url": TEST_URL, "credentials": {"identifier": "1234", "password": "1234"}, "max_fetch": 100, "max_fetch_domain": 100, "max_fetch_creds": 100, "event_types_to_fetch": [REPORT.name], "is_fetch_events": True, } @pytest.fixture def params_mocker(monkeypatch): """ Patch demisto.params so tests can mutate a single dict in-place. """ params_holder: dict = DEFAULT_PARAMS.copy() monkeypatch.setattr(demisto, "params", lambda: params_holder) return params_holder @pytest.fixture() def client() -> Client: return Client( TEST_URL, client_id="1234", client_secret="1234", verify=False, proxy=False, ) def load_test_data(file_name): with open(f"test_data/{file_name}.json") as file: return json.load(file) class HttpRequestsMocker: """ Mocker for the HttpRequests. Uses examples of real response structures with mocked data. """ def __init__(self, num_of_events: int): self.num_of_events = num_of_events self.num_of_calls = 0 def valid_http_request_side_effect(self, method: str, url_suffix: str = "", params: Dict | None = None, **kwargs): """ Create Response with the relevant data depend on the params. """ if method == "GET": if url_suffix == REPORT.url_suffix: start_date = params.get("start-date") events = create_report_events(1, amount_of_events=self.num_of_events, start_date=start_date) elif url_suffix == DOMAIN.url_suffix: skip = int(params.get("skip", 0)) limit = int(params.get("limit", 0)) events = create_domain_events(limit, skip) elif url_suffix == CREDENTIALS.url_suffix: limit = int(params.get("limit", 0)) events = create_creds_events(limit) return create_mocked_response(events) elif method == "POST" and kwargs.get("full_url") == "https://auth.cybelangel.com/oauth/token": return {"access_token": "new_access_token"} return None def valid_http_request_zero_events_side_effect(self, method: str, url_suffix: str = "", params: Dict | None = None, **kwargs): """ Return Empty response """ if method == "GET": if url_suffix == REPORT.url_suffix: events = {"reports": []} elif url_suffix == DOMAIN.url_suffix: events = {"results": [], "total": 0} elif url_suffix == CREDENTIALS.url_suffix: events = [] return create_mocked_response(events) elif method == "POST" and kwargs.get("full_url") == "https://auth.cybelangel.com/oauth/token": return {"access_token": "new_access_token"} return None def expired_token_http_request_side_effect( self, method: str, url_suffix: Optional[str] = None, params: Dict | None = None, **kwargs ): """ Mock the behavior of first call is with expired token. """ if method == "GET" and url_suffix == "/api/v2/reports": if self.num_of_calls == 0: self.num_of_calls += 1 return create_mocked_response([], status_code=401) start_date = params.get("start-date") return create_report_events(1, amount_of_events=self.num_of_events, start_date=start_date) if method == "POST" and kwargs.get("full_url") == "https://auth.cybelangel.com/oauth/token": return {"access_token": "new_access_token"} return None def create_report_events(start_id: int, amount_of_events: int, start_date: str) -> dict[str, list[dict]]: """Return {"reports": [...]} with shuffled `_time` and `id` fields.""" events = [ { REPORT.id_key: str(i), "updated_at": (dateparser.parse(start_date) + timedelta(seconds=i)).strftime(DATE_FORMAT), } for i in range(start_id, start_id + amount_of_events) ] random.shuffle(events) return {"reports": events} def create_domain_events(limit: int, skip: int) -> dict[str, Any]: """Return {"results": [...]} with reversed chronological order.""" response = load_test_data("domain_response") events = response["results"] total = response["total"] return {"results": events[skip : skip + limit], "total": total} def create_creds_events(limit: int) -> list[dict]: """Return a plain list of credential-watchlist events in ascending order.""" return load_test_data("credentials_response")[:limit] @pytest.fixture(autouse=True) def mock_get_id(monkeypatch): def mock_get_id(self, event): # Use the last key in id_key list, or id_key itself if it's a string if isinstance(self.id_key, list): return str(event.get(self.id_key[-1], "")) return str(event.get(self.id_key, "")) monkeypatch.setattr("CybelAngelEventCollector.EventType.get_id", mock_get_id) def create_mocked_response(response: List[Dict] | Dict, status_code: int = 200) -> requests.Response: mocked_response = requests.Response() mocked_response._content = json.dumps(response).encode("utf-8") mocked_response.status_code = status_code return mocked_response # --------- Test get last run logics -------------------------------------- def test_get_last_run_no_previous(mocker): """ Given: - demisto.getLastRun() is empty or None. When: - calling get_last_run() Then: - return dict with REPORT, DOMAIN and CREDENTIALS all initialized to now - 1 minute and empty ID lists. """ from CybelAngelEventCollector import get_last_run mocker.patch.object(demisto, "getLastRun", return_value={}) now = datetime(2025, 5, 15, 12, 0, 0) result = get_last_run(now, [REPORT, DOMAIN, CREDENTIALS]) expected_time = (now - timedelta(minutes=1)).strftime(DATE_FORMAT) for event_type in (REPORT, DOMAIN, CREDENTIALS): assert event_type.name in result assert result[event_type.name][LATEST_TIME] == expected_time assert result[event_type.name][LATEST_FETCHED_IDS] == [] def test_get_last_run_partial_existing(mocker): """ Given: - demisto.getLastRun() contain some events types. When: - Calling get_last_run() Then: - Only non existent types will get now - 1 minute and empty ID lists. """ from CybelAngelEventCollector import get_last_run existing_time = "2025-05-14T11:59:00" existing_ids = ["x", "y"] now = datetime(2025, 5, 15, 12, 0, 0) mocker.patch.object( demisto, "getLastRun", return_value={REPORT.name: {LATEST_TIME: existing_time, LATEST_FETCHED_IDS: existing_ids}}, ) result = get_last_run(now, [REPORT, DOMAIN, CREDENTIALS]) # REPORT must be unchanged assert result[REPORT.name][LATEST_TIME] == existing_time assert result[REPORT.name][LATEST_FETCHED_IDS] == existing_ids # DOMAIN and CREDENTIALS must now be set to now-1min expected_time = (now - timedelta(minutes=1)).strftime(DATE_FORMAT) for event_type in (DOMAIN, CREDENTIALS): assert result[event_type.name][LATEST_TIME] == expected_time assert result[event_type.name][LATEST_FETCHED_IDS] == [] def test_get_last_run_all_present(mocker): """ Given: - demisto.getLastRun() got all events. When: - Calling get_last_run() Then: - get_lat_run will not change any of the existent. """ from CybelAngelEventCollector import get_last_run now = datetime(2025, 5, 15, 12, 0, 0) initial = { REPORT.name: {LATEST_TIME: "2025-05-14T11:00:00", LATEST_FETCHED_IDS: [1]}, DOMAIN.name: {LATEST_TIME: "2025-05-14T11:01:00", LATEST_FETCHED_IDS: [2]}, CREDENTIALS.name: {LATEST_TIME: "2025-05-14T11:02:00", LATEST_FETCHED_IDS: [3]}, } mocker.patch.object(demisto, "getLastRun", return_value=initial.copy()) result = get_last_run(now, [REPORT, DOMAIN, CREDENTIALS]) # Should be exactly the same dict we passed in assert result == initial def test_get_last_run_all_present_one_removed(mocker): """ Given: - getLastRun() already contains all three types - event_types_to_fetch have one less event. When: - Calling get_last_run(). Then: - Reset the time and id of the event who removed, other as usual. """ from CybelAngelEventCollector import get_last_run now = datetime(2025, 5, 15, 12, 0, 0) last_time = now - timedelta(minutes=1) initial = { REPORT.name: {LATEST_TIME: "2025-05-14T11:00:00", LATEST_FETCHED_IDS: [1]}, DOMAIN.name: {LATEST_TIME: "2025-05-14T11:01:00", LATEST_FETCHED_IDS: [2]}, CREDENTIALS.name: {LATEST_TIME: "2025-05-14T11:02:00", LATEST_FETCHED_IDS: [3]}, } mocker.patch.object(demisto, "getLastRun", return_value=initial.copy()) result = get_last_run(now, [REPORT, DOMAIN]) initial[CREDENTIALS.name] = {LATEST_TIME: last_time.strftime(DATE_FORMAT), LATEST_FETCHED_IDS: []} assert result == initial # --------- Test Token -------------------------------------- def test_http_request_token_expired(client: Client, mocker): """ When calling http_request and the token is expired, will ask for a new one. Given: - expired token from integration context When: - retrieving events by a http-request Then: - make sure token is replaced with a new access token - make sure events are still returned even when token has expired """ http_mocker = HttpRequestsMocker(1) mocker.patch.object(client, "_http_request", side_effect=http_mocker.expired_token_http_request_side_effect) mocker.patch.object(demisto, "getIntegrationContext", return_value={"access_token": "old_access_token"}) set_integration_context_mocker: MagicMock = mocker.patch.object(demisto, "setIntegrationContext") result = client.http_request(method="GET", url_suffix="/api/v2/reports", params={"start-date": "2021-01-10T00:00:00"}) events = result["reports"] assert len(events) == 1 assert set_integration_context_mocker.call_args[0][0] == {"access_token": "new_access_token"} def test_get_token_request_raises(monkeypatch): """ Given: - A Client whose _http_request returns {} (no access_token) When: - Calling get_token_request() Then: - RuntimeError is raised with the correct message """ client = Client("u", "i", "s", verify=False, proxy=False) monkeypatch.setattr(client, "_http_request", lambda *a, **k: {}) with pytest.raises(RuntimeError) as ei: client.get_token_request() assert "Could not retrieve token" in str(ei.value) # --------- Test Commands -------------------------------------- def test_the_test_module(mocker, params_mocker): """ Given: - valid credentials When: - running the test-module Then: - make sure "ok" is returned """ import CybelAngelEventCollector return_results_mocker: MagicMock = mocker.patch.object(CybelAngelEventCollector, "return_results") params_mocker["max_fetch"] = 10 params_mocker["event_types_to_fetch"] = [REPORT.name, CREDENTIALS.name, DOMAIN.name] mocker.patch.object(demisto, "command", return_value="test-module") http_mocker = HttpRequestsMocker(10) mocker.patch.object(CybelAngelEventCollector.Client, "_http_request", side_effect=http_mocker.valid_http_request_side_effect) CybelAngelEventCollector.main() assert return_results_mocker.called assert return_results_mocker.call_args[0][0] == "ok" def test_get_events_command_command(mocker, params_mocker): """ Given: - limit is 9. - server holds 10 events. - should_push_events = true. - fetching reports. When: - running the fetch-events Then: - all first 9 events are sent to xsiam. """ import CybelAngelEventCollector mocker.patch.object(demisto, "getLastRun", return_value={}) params_mocker["event_types_to_fetch"] = [REPORT.name] params_mocker["max_fetch"] = 100 mocker.patch.object( demisto, "args", return_value={"start_date": "2024-02-29T13:48:32", "limit": 9, "should_push_events": True}, ) mocker.patch.object(demisto, "command", return_value="cybelangel-get-events") http_mocker = HttpRequestsMocker(10) mocker.patch.object(CybelAngelEventCollector.Client, "_http_request", side_effect=http_mocker.valid_http_request_side_effect) mock_send_events = mocker.patch.object(CybelAngelEventCollector, "send_events_to_xsiam") CybelAngelEventCollector.main() mock_send_events.assert_called_once() _, call_kwargs = mock_send_events.call_args events_sent_to_xsiam = call_kwargs["events"] assert len(events_sent_to_xsiam) == 9 assert [f"{i}" for i in range(1, 10)] == [event.get(REPORT.id_key) for event in events_sent_to_xsiam] assert call_kwargs["vendor"] == CybelAngelEventCollector.VENDOR assert call_kwargs["product"] == CybelAngelEventCollector.PRODUCT def test_cybelangel_report_list_command(mocker, client: Client): """ Given: - A start date and an end date. When: - Retrieving a list of reports within the specified date range. Then: - Ensure the command returns a valid list of reports. - Validate that the outputs are correctly formatted. """ from CybelAngelEventCollector import cybelangel_report_list_command data = load_test_data("report_list") reports = data["reports"] mocker.patch.object( client, "get_reports_list", return_value=reports, ) args = {"start_date": "2024-01-01", "end_date": "2024-02-01"} result = cybelangel_report_list_command(client, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "CybelAngel.Report" assert result.outputs == reports assert "Reports list" in result.readable_output def test_cybelangel_report_get_command(mocker, client: Client): """ Given: - A specific report ID. When: - Retrieving the details of the report. Then: - Ensure the command returns the correct report details. - Validate that the readable output includes the report ID. """ from CybelAngelEventCollector import cybelangel_report_get_command mocker.patch.object( client, "_http_request", return_results=load_test_data("report_list").get("reports")[0], ) args = {"report_id": "test"} result = cybelangel_report_get_command(client, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "CybelAngel.Report" assert result.outputs is not None assert "Report ID" in result.readable_output def test_cybelangel_report_get_command_to_pdf(mocker, client: Client): """ Given: - A report ID and the 'pdf' flag set to true. When: - Requesting to export the report as a PDF. Then: - Ensure the command returns a valid file result in PDF format. """ from CybelAngelEventCollector import cybelangel_report_get_command mocker.patch.object( client, "_http_request", return_results=load_test_data("report_list").get("reports")[0], ) # test get report to pdf args = {"report_id": "test", "pdf": "true"} mocker.patch( "CybelAngelEventCollector.fileResult", return_value={ "Contents": "", "ContentsFormat": "text", "Type": 9, "File": "cybelangel_report_<report_id>.pdf", "FileID": "<report_id>", }, ) result = cybelangel_report_get_command(client, args) assert isinstance(result, dict) def test_cybelangel_mirror_report_get_command(mocker, client: Client): """ Given: - A report ID with the 'csv' flag set to false. When: - Fetching mirror report details. Then: - Ensure the command returns a CommandResults object with the expected report data. """ from CybelAngelEventCollector import cybelangel_mirror_report_get_command mocker.patch.object( client, "_http_request", return_results=load_test_data("mirror-report"), ) args = {"csv": "false", "report_id": "test"} result = cybelangel_mirror_report_get_command(client, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "CybelAngel.ReportMirror" assert result.outputs is not None assert "Mirror details for Report ID" in result.readable_output def test_cybelangel_mirror_report_get_command_to_csv(mocker, client: Client): """ Given: - A report ID with the 'csv' flag set to true. When: - Requesting to export the mirror report as a CSV file. Then: - Ensure the command returns a valid file result in CSV format. """ from CybelAngelEventCollector import cybelangel_mirror_report_get_command mocker.patch.object( client, "_http_request", return_results=load_test_data("mirror-report"), ) args = {"report_id": "test", "csv": "true"} mocker.patch( "CybelAngelEventCollector.fileResult", return_value={ "Contents": "", "ContentsFormat": "text", "Type": 9, "File": "cybelangel_mirror_report_<report_id>.csv", "FileID": "<report_id>", }, ) result = cybelangel_mirror_report_get_command(client, args) assert isinstance(result, dict) def test_cybelangel_report_comment_create_command(mocker, client: Client): """ Given: - A discussion ID, comment content, and additional metadata. When: - Creating a new comment for the report. Then: - Ensure the command successfully adds the comment and returns the expected output. """ from CybelAngelEventCollector import cybelangel_report_comment_create_command, Client report_id = "11223344" mocker.patch.object( Client, "get_report_comment", return_value=load_test_data("create_comment_result"), ) args = {"discussion_id": f"{report_id}:tenant id", "content": "Test func", "parent_id": "55667788", "assigned": "true"} response = cybelangel_report_comment_create_command(client, args) assert f"Comment created successfully for report ID: {report_id}" in response.readable_output def test_cybelangel_report_comment_create_command_invalid(mocker, client: Client): """ Given: - An invalid discussion ID that does not follow the 'report_id:tenant_id' format. When: - Attempting to create a comment with the invalid discussion ID. Then: - Ensure the command raises a ValueError with the correct error message. """ from CybelAngelEventCollector import cybelangel_report_comment_create_command report_id = "11223344" # Case: Invalid discussion_id format (no colon) args_invalid = {"discussion_id": report_id, "content": "Test func"} with pytest.raises(ValueError, match="Invalid discussion_id format. Expected format: 'report_id:tenant_id'."): cybelangel_report_comment_create_command(client, args_invalid) def test_cybelangel_archive_report_by_id_get_command(mocker, client: Client): """ Given: - A report ID to retrieve the archived version of the report. When: - Requesting the archived report in ZIP format. Then: - Ensure the command returns a file result containing the ZIP archive. - Validate that the returned file name follows the expected format. """ from CybelAngelEventCollector import cybelangel_archive_report_by_id_get_command mocker.patch.object( client, "_http_request", return_results=load_test_data("mirror-report"), ) args = {"report_id": "test"} mocker.patch( "CybelAngelEventCollector.fileResult", return_value={ "Contents": "", "ContentsFormat": "text", "Type": 9, "File": "cybelangel_archive_report_<report_id>.zip", "FileID": "<report_id>", }, ) result = cybelangel_archive_report_by_id_get_command(client, args) assert isinstance(result, dict) def test_cybelangel_report_comments_get_command(mocker, client: Client): """ Given: - A report ID for which comments need to be retrieved. - A response containing existing comments for the report. When: - Running the `cybelangel_report_comments_get_command`. Then: - Ensure the command successfully retrieves comments for the given report. - Validate that the `discussion_id` starts with the report ID. - Validate that the `discussion_id` ends with 'Tenant id'. """ from CybelAngelEventCollector import cybelangel_report_comments_get_command, Client # case No previous comments exist in this report mocker.patch.object( Client, "get_report_comment", return_value=load_test_data("get_comments_res"), ) report_id = "11223344" args = {"report_id": report_id} response = cybelangel_report_comments_get_command(client, args) assert response.outputs.get("Comment")[0].get("discussion_id").startswith(report_id) # type: ignore assert response.outputs.get("Comment")[0].get("discussion_id").endswith("Tenant id") # type: ignore def test_cybelangel_report_attachment_get_command(mocker, client: Client): """ Given: - report ID and attachment ID When: - running the cybelangel_report_attachment_get_command with the given arguments Then: - ensure the function returns a dictionary containing the expected file details and the text of the attachment starts with "sep=" for CSV file. """ from CybelAngelEventCollector import cybelangel_report_attachment_get_command, Client response = mocker.patch.object( Client, "get_report_attachment", return_value=type( "StringWrapper", (object,), {"text": "sep=,\nkeyword,email,password\nTest1,Test2,Test3\nTest1,Test2"} )(), ) report_id = "11223344" attachment_id = "55667788" args = {"report_id": report_id} mocker.patch( "CybelAngelEventCollector.fileResult", return_value={ "Contents": "", "ContentsFormat": "text", "Type": 9, "File": f"cybelangel_report_{report_id}_attachment_{attachment_id}.csv", "FileID": "<report_id>", }, ) result = cybelangel_report_attachment_get_command(client, args) assert isinstance(result, dict) assert response.text.startswith("sep=") # --------- Test Fetching Logics -------------------------------------- @pytest.mark.parametrize( "event_type, max_fetch_key", [ (REPORT.name, "max_fetch"), (CREDENTIALS.name, "max_fetch_creds"), (DOMAIN.name, "max_fetch_domain"), ], ) def test_fetch_events_no_last_run(mocker, event_type, max_fetch_key, params_mocker): """ Given: - no last run (first time of the fetch). - server holds 10 events from each type. When: - running the fetch-events. Then: - make sure events are sent into xsiam. - make sure all the 10 events are fetched. - make sure last run is updated. """ import CybelAngelEventCollector from CybelAngelEventCollector import normalize_date_format send_events_mocker = mocker.patch.object(CybelAngelEventCollector, "send_events_to_xsiam") set_last_run_mocker = mocker.patch.object(demisto, "setLastRun", return_value={}) params_mocker["event_types_to_fetch"] = event_type params_mocker[max_fetch_key] = 10 mocker.patch.object(demisto, "command", return_value="fetch-events") http_mocker = HttpRequestsMocker(10) mocker.patch.object(CybelAngelEventCollector.Client, "_http_request", side_effect=http_mocker.valid_http_request_side_effect) CybelAngelEventCollector.main() assert send_events_mocker.called fetched_events = send_events_mocker.call_args[0][0] assert len(fetched_events) == 10 assert set_last_run_mocker.called last_run = set_last_run_mocker.call_args[0][0] max_event_index = EVENT_TYPE[event_type].max_index assert last_run[event_type][LATEST_TIME] == normalize_date_format(fetched_events[max_event_index]["_time"]) last_id = EVENT_TYPE[event_type].get_id(fetched_events[max_event_index]) assert last_run[event_type][LATEST_FETCHED_IDS][0] == last_id def test_fetch_events_token_expired(mocker): """ Given: - token that has expired. When: - running the fetch-events. Then: - make sure events are sent into xsiam - make sure all the 10 events are fetched. - make sure last run is updated. - make sure the new access token is getting into the integration context. """ import CybelAngelEventCollector from CybelAngelEventCollector import REPORT, LATEST_TIME, LATEST_FETCHED_IDS send_events_mocker: MagicMock = mocker.patch.object(CybelAngelEventCollector, "send_events_to_xsiam") set_last_run_mocker: MagicMock = mocker.patch.object(demisto, "setLastRun", return_value={}) mocker.patch.object(demisto, "getLastRun", return_value={}) mocker.patch.object(demisto, "command", return_value="fetch-events") mocker.patch.object(demisto, "getIntegrationContext", return_value={"access_token": "old_access_token"}) set_integration_context_mocker: MagicMock = mocker.patch.object(demisto, "setIntegrationContext") http_mocker = HttpRequestsMocker(10) mocker.patch.object( CybelAngelEventCollector.Client, "_http_request", side_effect=http_mocker.expired_token_http_request_side_effect ) CybelAngelEventCollector.main() assert send_events_mocker.called fetched_events = send_events_mocker.call_args[0][0] assert len(fetched_events) == 10 assert set_last_run_mocker.called last_run = set_last_run_mocker.call_args[0][0] assert last_run[REPORT.name][LATEST_TIME] == fetched_events[-1]["_time"] assert last_run[REPORT.name][LATEST_FETCHED_IDS][0] == fetched_events[-1]["id"] assert set_integration_context_mocker.call_args[0][0] == {"access_token": "new_access_token"} @pytest.mark.parametrize( "event_type, max_fetch_key", [ (REPORT.name, "max_fetch_reports"), (CREDENTIALS.name, "max_fetch_creds"), (DOMAIN.name, "max_fetch_domain"), ], ) def test_fetch_events_with_last_run(mocker, max_fetch_key, event_type, params_mocker): """ Given: - last run of fetched events IDs [1, 2]. - server holds 20 events for each type. When: - running the fetch-events. Then: - make sure events are sent into xsiam. - make sure all the 18 events are fetched, the rest were not fetched because they were fetched in previous fetch. - make sure last run is updated. """ import CybelAngelEventCollector from CybelAngelEventCollector import normalize_date_format send_events_mocker: MagicMock = mocker.patch.object(CybelAngelEventCollector, "send_events_to_xsiam") set_last_run_mocker: MagicMock = mocker.patch.object(demisto, "setLastRun", return_value={}) mocker.patch.object( demisto, "getLastRun", return_value={ event_type: { LATEST_TIME: "2024-02-29T13:48:32", LATEST_FETCHED_IDS: ["1", "2"], } }, ) params_mocker["event_types_to_fetch"] = [event_type] mocker.patch.object(demisto, "command", return_value="fetch-events") http_mocker = HttpRequestsMocker(20) mocker.patch.object(CybelAngelEventCollector.Client, "_http_request", side_effect=http_mocker.valid_http_request_side_effect) CybelAngelEventCollector.main() assert send_events_mocker.called fetched_events = send_events_mocker.call_args[0][0] assert len(fetched_events) == 18 assert set_last_run_mocker.called last_run = set_last_run_mocker.call_args[0][0] max_event_index = EVENT_TYPE[event_type].max_index assert last_run[event_type][LATEST_TIME] == normalize_date_format(fetched_events[max_event_index]["_time"]) last_id = EVENT_TYPE[event_type].get_id(fetched_events[max_event_index]) assert last_run[event_type][LATEST_FETCHED_IDS][0] == last_id assert all(event["SOURCE_LOG_TYPE"] == EVENT_TYPE[event_type].source_log_type for event in fetched_events) @pytest.mark.parametrize( "last_run", [ {}, # Empty Last run { REPORT.name: {LATEST_TIME: "2024-02-29T13:48:32", LATEST_FETCHED_IDS: [1, 2]}, CREDENTIALS.name: {LATEST_TIME: "2024-02-29T13:48:32", LATEST_FETCHED_IDS: [1, 2]}, DOMAIN.name: {LATEST_TIME: "2024-02-29T13:48:32", LATEST_FETCHED_IDS: [1, 2]}, }, ], ) def test_fetch_events_no_events(mocker, last_run): """ Given: - no last run. - no new events have been received from the api. When: - running the fetch-events. Then: - make sure no events are sent into xsiam. - make sure last run is returned with the last run updated. """ import CybelAngelEventCollector send_events_mocker: MagicMock = mocker.patch.object(CybelAngelEventCollector, "send_events_to_xsiam") set_last_run_mocker: MagicMock = mocker.patch.object(demisto, "setLastRun", return_value={}) mocker.patch.object(demisto, "command", return_value="fetch-events") http_mocker = HttpRequestsMocker(0) mocker.patch.object( CybelAngelEventCollector.Client, "_http_request", side_effect=http_mocker.valid_http_request_zero_events_side_effect ) CybelAngelEventCollector.main() assert send_events_mocker.called fetched_events = send_events_mocker.call_args[0][0] assert len(fetched_events) == 0 assert set_last_run_mocker.called actual_last_run = set_last_run_mocker.call_args[0][0] for event_type in (REPORT, CREDENTIALS, DOMAIN): assert event_type.name in actual_last_run ts = actual_last_run[event_type.name][LATEST_TIME] assert ts assert isinstance(ts, str) assert actual_last_run[event_type.name][LATEST_FETCHED_IDS] == [] @pytest.mark.parametrize( "event_type, max_fetch_key", [ (REPORT.name, "max_fetch_reports"), (CREDENTIALS.name, "max_fetch_creds"), (DOMAIN.name, "max_fetch_domain"), ], ) def test_fetch_events_with_last_run_dedup_event(mocker, event_type, max_fetch_key, params_mocker): """ Given: - last run with events that was already fetched. - API return events already fetched last time. - no "new" events have been received from the api. When: - running the fetch-events. Then: - make sure no events are sent into xsiam. - make sure last run does not get updated. """ import CybelAngelEventCollector send_events_mocker: MagicMock = mocker.patch.object(CybelAngelEventCollector, "send_events_to_xsiam") set_last_run_mocker: MagicMock = mocker.patch.object(demisto, "setLastRun") num_events = 5 last_ids = [str(i) for i in range(1, 21)] initial_time = "2025-01-01T00:00:00" mocker.patch.object( demisto, "getLastRun", return_value={ event_type: { LATEST_TIME: initial_time, LATEST_FETCHED_IDS: last_ids, } }, ) params_mocker[max_fetch_key] = 5 params_mocker["event_types_to_fetch"] = event_type mocker.patch.object(demisto, "command", return_value="fetch-events") http_mocker = HttpRequestsMocker(num_events) mocker.patch.object(CybelAngelEventCollector.Client, "_http_request", side_effect=http_mocker.valid_http_request_side_effect) CybelAngelEventCollector.main() assert send_events_mocker.called fetched_events = send_events_mocker.call_args[0][0] assert len(fetched_events) == 0 assert set_last_run_mocker.called actual_last_run = set_last_run_mocker.call_args[0][0] assert event_type in actual_last_run assert actual_last_run[event_type][LATEST_FETCHED_IDS] == [] new_time = actual_last_run[event_type][LATEST_TIME] assert new_time != initial_time def test_fetch_events_domain_two_call_paging(mocker, params_mocker): """ Given: - Last run holds ids [1,2]. - 10 total domain events in the 1-minute window. - max_fetch_domain = 6. When: - running fetch-events. Then: - send_events_to_xsiam is called with exactly 6 events. - Those 6 are with IDs 3..8. - lastRun is set to the time & ID of event 8. """ import CybelAngelEventCollector send_events = mocker.patch.object(CybelAngelEventCollector, "send_events_to_xsiam") set_last_run = mocker.patch.object(demisto, "setLastRun") mocker.patch.object( demisto, "getLastRun", return_value={DOMAIN.name: {LATEST_TIME: datetime.now().isoformat(), LATEST_FETCHED_IDS: ["1", "2"]}}, ) params_mocker["event_types_to_fetch"] = DOMAIN.name params_mocker["max_fetch_domain"] = 6 mocker.patch.object(demisto, "command", return_value="fetch-events") http_mocker = HttpRequestsMocker(10) mocker.patch.object(Client, "_http_request", side_effect=http_mocker.valid_http_request_side_effect) CybelAngelEventCollector.main() assert send_events.called, "events should have been sent" fetched = send_events.call_args[0][0] assert len(fetched) == 6 assert fetched[0]["SOURCE_LOG_TYPE"] == DOMAIN.source_log_type returend_ids = [DOMAIN.get_id(event) for event in fetched] assert returend_ids == [str(i) for i in range(8, 2, -1)] lr = set_last_run.call_args[0][0] assert lr[DOMAIN.name][LATEST_TIME] == fetched[0]["_time"] last_id = DOMAIN.get_id(fetched[0]) assert lr[DOMAIN.name][LATEST_FETCHED_IDS][0] == last_id def test_get_latest_event_time_and_ids(): """ Given: - Server holds 6 events with the same timestamp. - 3 events already fetched. - fetching 3 additional events. When: - Calling get_latest_event_time_and_ids. Then: - last_ids holds all 6 events. - last_time does not changes. """ from CybelAngelEventCollector import get_latest_event_time_and_ids last_run_time = "2024-02-29T13:48:32" events = [ { REPORT.id_key: f"{i}", # type: ignore "_time": last_run_time, } for i in range(4, 7) ] last_run_ids = ["1", "2", "3"] last_time, last_ids = get_latest_event_time_and_ids( events=events, event_type=REPORT, last_run_time=last_run_time, last_run_ids=last_run_ids ) assert last_time == last_run_time assert len(last_ids) == 6 assert set(last_ids) == {"1", "2", "3", "4", "5", "6"} def test_fetch_events_same_timestamp(client, mocker): """ Given: - Server holds 6 events with the same timestamp. - 3 events already fetched in the last run. When: - Calling fetch_events Then: - get_domain_watchlist called with limit = 6. - Only events 4,5,6 are sent back from the function. - lastRun is set to the same time with all 6 ids. """ from CybelAngelEventCollector import fetch_events, DOMAIN, LATEST_TIME, LATEST_FETCHED_IDS last_run_time = "2024-02-29T13:48:32" last_run_ids = ["1", "2", "3"] mocker.patch.object( demisto, "getLastRun", return_value={DOMAIN.name: {LATEST_TIME: last_run_time, LATEST_FETCHED_IDS: last_run_ids}} ) captured = {} def fake_get_domain_watchlist(start_date, end_date, limit, event_type=None): # noqa: D401 captured["limit"] = limit # six synthetic events, all with identical _time return [{"domain": str(i), "_time": last_run_time} for i in range(1, limit + 1)] mocker.patch.object(client, "get_domain_watchlist", side_effect=fake_get_domain_watchlist) DOMAIN.max_fetch = 3 events, new_last_run = fetch_events(client, [DOMAIN]) assert captured["limit"] == 6 assert [DOMAIN.get_id(e) for e in events] == ["4", "5", "6"] assert new_last_run[DOMAIN.name][LATEST_TIME] == last_run_time + "Z" assert set(new_last_run[DOMAIN.name][LATEST_FETCHED_IDS]) == {str(i) for i in range(1, 7)}