CybelAngel
This integration connects your alerts from CybelAngel.
Data Enrichment & Threat Intelligence · CybelAngel
Details
| ID | CybelAngel |
|---|---|
| Provider | CybelAngel |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
CybelAngel XSOAR Integration
This integration enables Cortex XSOAR to fetch and manage alerts from the CybelAngel platform, allowing security teams to monitor and respond to digital risk exposure incidents.
Configuration
Prerequisites
- CybelAngel API client ID and secret
- CybelAngel tenant ID
- XSOAR platform version 6.0.0 or later
Setup Instructions
- Navigate to Settings > Integrations > Servers & Services
- Search for CybelAngel
- Click Add instance
- Input the following parameters:
- Client ID
- Client Secret
- Tenant ID
- First fetch interval (optional)
Commands
Fetch Incidents
Automatically fetches new CybelAngel alerts as XSOAR incidents.
cybelangel-get-report-by-id
Retrieves detailed information about a specific report.
!cybelangel-get-report-by-id report_id=<report_id>
cybelangel-get-report-attachment
Downloads an attachment from a specified report.
!cybelangel-get-report-attachment report_id=<report_id> attachment_id=<attachment_id> filename=<filename>
cybelangel-remediate
Creates a remediation request for a specific report.
!cybelangel-remediate report_id=<report_id> email=<email> requester_fullname=<name>
cybelangel-get-comments
Retrieves comments associated with a report.
!cybelangel-get-comments report_id=<report_id>
cybelangel-post-comment
Adds a comment to a specified report.
!cybelangel-post-comment report_id=<report_id> comment=<comment>
cybelangel-update-status
Updates the status of a report.
!cybelangel-update-status report_id=<report_id> status=<status>
cybelangel-get-report-pdf
Downloads the PDF version of a report.
!cybelangel-get-report-pdf report_id=<report_id>
Troubleshooting
Authentication Issues
- Verify API credentials are correct
- Check token expiration (tokens automatically refresh after 1 hour)
- Ensure proper network connectivity to CybelAngel endpoints
Incident Fetching
- First fetch interval determines initial data pull window
- Subsequent fetches use last run time
- Check logs for any API errors or rate limiting issues
Known Limitations
- Maximum fetch interval is capped at 500 minutes
- PDF reports may take longer to download for large files
- Attachments must be downloaded individually
Configuration parameters
credentials— CybelAngel Client ID (required)tenant_id— CybelAngel Tenant ID (required)first_fetch— First fetch Interval (Select amount of days to fetch back in time) (required)base_url— Base URLmax_fetch— Max FetchisFetch— Fetch incidentsincidentFetchInterval— Incidents Fetch IntervalincidentType— Incident type
Commands (7)
-
cybelangel-get-commentsGet comments for a report.
-
cybelangel-get-report-attachmentGet an attachment of a report.
-
cybelangel-get-report-by-idGet a report by ID.
-
cybelangel-get-report-pdfDownloads a PDF report from CybelAngel by report ID and saves it to the War Room.
-
cybelangel-post-commentPost a comment on a report.
-
cybelangel-remediateRemediate a report by ID.
-
cybelangel-update-statusUpdate the status of a report.
import json from datetime import datetime, timedelta, UTC import pytest from CommonServerPython import * from CybelAngel import ( Client, get_report_by_id_command, post_comment_command, get_comments_command, remediate_command, get_report_pdf_command, _datetime_helper, _set_context, get_report_attachment_command, update_status_command, fetch_incidents, test_module, ) BASE_URL = "https://platform.cybelangel.com/" DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ" def load_mock_response(file_name: str) -> str: with open(f"test_data/{file_name}", encoding="utf-8") as mock_file: return mock_file.read() @pytest.fixture def client(): """Fixture to create a CybelAngel Client instance.""" return Client( client_id="test_id", client_secret="test_secret", auth_token="test_token", ) def test_fetch_incidents(client, requests_mock): mock_response = load_mock_response("fetch_incidents.json") requests_mock.get(f"{BASE_URL}api/v2/reports", json=json.loads(mock_response)) last_run = {"start_time": (datetime.now(UTC) - timedelta(days=1)).strftime(DATE_FORMAT)} incidents = fetch_incidents(client, first_fetch=True, last_run=last_run, first_fetch_interval=1440) assert len(incidents) == 4 raw_json = json.loads(incidents[0]["rawJSON"]) assert raw_json["id"] == "8bb43081-3349-4c89-b301-8dff0dcca56e" assert incidents[1]["severity"] == 3 def test_get_report_by_id_command(client, requests_mock): """ Test the `get_report_by_id_command` function for retrieving a report by ID. """ mock_response = load_mock_response("get_report_by_id.json") requests_mock.get(f"{BASE_URL}api/v2/reports/test-report-id", json=json.loads(mock_response)) args = {"report_id": "test-report-id"} result = get_report_by_id_command(client, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "CybelAngel.Report" assert result.outputs["id"] == "test-report-id" assert result.outputs["title"] == "Test Report Title" def test_fetch_incidents_empty_response(client, requests_mock): """ Test the `fetch_incidents` function when there are no incidents to return. """ mock_response = load_mock_response("empty_response.json") requests_mock.get(f"{BASE_URL}api/v2/reports", json=json.loads(mock_response)) last_run = {"start_time": (datetime.now(UTC) - timedelta(days=1)).strftime(DATE_FORMAT)} incidents = fetch_incidents(client, first_fetch=True, last_run=last_run, first_fetch_interval=1440) assert len(incidents) == 0 def test_fetch_incidents_no_last_run(client, requests_mock): """ Test the `fetch_incidents` function when fetching incidents for the first time. """ mock_response = load_mock_response("fetch_incidents.json") requests_mock.get(f"{BASE_URL}api/v2/reports", json=json.loads(mock_response)) last_run = "" incidents = fetch_incidents(client, first_fetch=True, last_run=last_run, first_fetch_interval=1440) assert len(incidents) == 4 raw_json = json.loads(incidents[0]["rawJSON"]) assert raw_json["id"] == "8bb43081-3349-4c89-b301-8dff0dcca56e" assert incidents[1]["severity"] == 3 def test_get_report_attachment(client, requests_mock): """ Test the `get_report_attachment` function for retrieving a report attachment. """ mock_file_content = b"Sample Attachment Content" requests_mock.get(f"{BASE_URL}api/v1/reports/test-report-id/attachments/test-attachment-id", content=mock_file_content) attachment = client.get_report_attachment("test-report-id", "test-attachment-id") assert attachment == mock_file_content def test_update_status(client, requests_mock): """ Test the `update_status` function for updating the status of a report. """ mock_response = {"status": "success"} requests_mock.put(f"{BASE_URL}api/v1/reports/test-report-id/status", json=mock_response) status, status_code = client.update_status("resolved", "test-report-id") assert status_code == 200 output_status = json.loads(status) assert output_status["status"] == "success" # NEW Tests def test_remediate_command(client, requests_mock): """ Test the `remediate_command` function for submitting a remediation request. """ mock_response = {"status": "remediation_requested"} requests_mock.post(f"{BASE_URL}api/v1/reports/remediation-request", json=mock_response) args = {"report_id": "test-report-id", "email": "user@example.com", "requester_fullname": "John Doe"} result = remediate_command(client, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "CybelAngel.Remediation" assert result.readable_output == "Remediation Status test-report-id : Successful" def test_get_comments_command(client, requests_mock): """ Test the `get_comments_command` function for retrieving comments on a report. """ mock_response = load_mock_response("get_comments.json") requests_mock.get(f"{BASE_URL}api/v1/reports/test-report-id/comments", json=json.loads(mock_response)) args = {"report_id": "test-report-id"} result = get_comments_command(client, args) assert isinstance(result, CommandResults) assert result.outputs_prefix == "CybelAngel.Comments" assert len(result.outputs) == 1 assert result.outputs[0]["content"] == "Body of the comment" assert result.outputs[0]["author"]["firstname"] == "John" assert result.outputs[0]["author"]["lastname"] == "Doe" assert result.outputs[0]["id"] == "3500bb64-6081-4cf5-8e6f-dca82dab4982" def test_post_comment_command(client, requests_mock): """ Test the `post_comment_command` function for adding a comment to a report. """ mock_response = {"status": "comment_posted"} requests_mock.post(f"{BASE_URL}api/v1/reports/test-report-id/comments", json=mock_response) args = {"report_id": "test-report-id", "comment": "This is a test comment", "tenant_id": "test-tenant-id"} result = post_comment_command(client, args["tenant_id"], args) assert isinstance(result, CommandResults) assert result.readable_output == "Comment added to report test-report-id: This is a test comment : STATUS: 200" def test_get_report_pdf_command(client, requests_mock): """ Test the `get_report_pdf_command` function for retrieving a report PDF. """ mock_pdf_content = b"%PDF-1.4 Sample PDF content" requests_mock.get(f"{BASE_URL}api/v1/reports/test-report-id/pdf", content=mock_pdf_content) args = {"report_id": "test-report-id"} result = get_report_pdf_command(client, args) assert isinstance(result, dict) assert result["Type"] == EntryType.FILE assert result["File"] == "test-report-id.pdf" # Validate the returned data matches what was mocked assert result["FileID"] is not None # Ensure that FileID is generated for the PDF def test_token_error_handling(client, requests_mock): requests_mock.post("https://auth.cybelangel.com/oauth/token", status_code=500) result = client.fetch_token() assert "Error fetching token" in result["msg"] def test_check_token_initial(client): """Test initial token check when token_time is None""" client.token_time = None client.check_token() assert client.token is not None def test_post_comment_with_parent(client, requests_mock): """Test posting a comment with a parent_id""" mock_response = {"status": "comment_posted"} requests_mock.post(f"{BASE_URL}api/v1/reports/test-report-id/comments", json=mock_response) response, status_code = client.post_comment( comment="Reply comment", report_id="test-report-id", tenant_id="test-tenant-id", parent_id="parent-comment-id" ) assert status_code == 200 def test_get_report_by_id_error(client, requests_mock): """Test error handling in get_report_by_id""" requests_mock.get(f"{BASE_URL}api/v2/reports/invalid-id", status_code=404) result = client.get_report_by_id("invalid-id") assert isinstance(result, list) assert "Error getting report" in result[0]["msg"] def test_get_comments_error(client, requests_mock): """Test error handling in get_comments""" requests_mock.get(f"{BASE_URL}api/v1/reports/test-id/comments", exc=requests.exceptions.HTTPError) with pytest.raises(SystemExit): client.get_comments("test-id") def test_test_module(client, requests_mock): mock_response = {"reports": [{"id": "test"}]} requests_mock.get(f"{BASE_URL}api/v2/reports", json=mock_response) assert test_module(client) == "ok" def test_test_module_error(client, requests_mock): """Testing error case for test_module""" requests_mock.get(f"{BASE_URL}api/v2/reports", status_code=403) client.token = None # Force auth error assert test_module(client) == "ok" def test_datetime_helper(): """Test the datetime helper function""" past_date = (datetime.now(UTC) - timedelta(minutes=30)).strftime(DATE_FORMAT) minutes = _datetime_helper(past_date) assert 29 <= minutes <= 31 # Allow small timing differences def test_set_context(client): client.new_token_fetched = True client.token = "test-token" client.token_time = "2024-02-06 12:00:00.000000" _set_context(client) context = demisto.getIntegrationContext() assert context["token"] == "test-token" assert context.get("first_pull") == "False" # Comparing strings # --- Command tests def test_get_report_attachment_command(client, requests_mock): mock_content = b"test attachment content" requests_mock.get(f"{BASE_URL}api/v1/reports/test-id/attachments/att-id", content=mock_content) result = get_report_attachment_command(client, {"report_id": "test-id", "attachment_id": "att-id", "filename": "test.txt"}) assert result["Type"] == EntryType.FILE assert result["File"] == "test.txt" def test_get_report_attachment_error(client, requests_mock): requests_mock.get(f"{BASE_URL}api/v1/reports/test-id/attachments/att-id", exc=Exception("Download failed")) result = get_report_attachment_command(client, {"report_id": "test-id", "attachment_id": "att-id", "filename": "test.txt"}) assert result.readable_output == "Error downloading attachment: Download failed" def test_update_status_command(client, requests_mock): mock_response = {"status": "updated"} requests_mock.put(f"{BASE_URL}api/v1/reports/test-id/status", json=mock_response) result = update_status_command(client, {"report_id": "test-id", "status": "resolved"}) assert result.outputs_prefix == "CybelAngel.StatusUpdate" assert "updated" in result.raw_response def test_update_status_error(client, requests_mock): requests_mock.put(f"{BASE_URL}api/v1/reports/test-id/status", exc=Exception("Update failed")) result = update_status_command(client, {"report_id": "test-id", "status": "resolved"}) assert "Error Updating status" in result.readable_output def test_fetch_incidents_with_invalid_response(client, requests_mock): requests_mock.get(f"{BASE_URL}api/v2/reports", json={"reports": []}) incidents = fetch_incidents(client, first_fetch=True, last_run="", first_fetch_interval=1) assert len(incidents) == 0 def test_get_report_pdf_command_missing_id(client): result = get_report_pdf_command(client, {}) assert result.readable_output == "Report ID not provided." def test_client_initialization(): """Test Client initialization with various parameter combinations""" # Test with minimum required parameters client1 = Client(client_id="test_id", client_secret="test_secret") assert client1.client_id == "test_id" assert client1.client_secret == "test_secret" assert client1.token == "" # Test with all parameters client2 = Client( client_id="test_id", client_secret="test_secret", auth_token="existing_token", token_time="2024-02-19T12:00:00Z" ) assert client2.token == "existing_token" assert client2.token_time == "2024-02-19T12:00:00Z" def test_fetch_token_success(requests_mock): """Test successful token fetch""" client = Client(client_id="test_id", client_secret="test_secret") mock_response = {"access_token": "new_token"} requests_mock.post("https://auth.cybelangel.com/oauth/token", json=mock_response) client.fetch_token() assert client.token == "Bearer new_token" assert client.new_token_fetched is True def test_get_reports_success(client, requests_mock): """Test successful retrieval of reports""" mock_response = {"reports": [{"id": "1", "title": "Report 1"}, {"id": "2", "title": "Report 2"}]} requests_mock.get(f"{BASE_URL}api/v2/reports", json=mock_response) reports = client.get_reports(interval=60) assert len(reports) == 2 assert reports[0]["id"] == "1" assert reports[1]["title"] == "Report 2" def test_get_all_reports_success(client, requests_mock): """Test successful retrieval of all reports""" mock_response = {"reports": [{"id": "1", "title": "Old Report"}, {"id": "2", "title": "Recent Report"}]} requests_mock.get(f"{BASE_URL}api/v2/reports", json=mock_response) reports = client.get_all_reports() assert len(reports) == 2 assert all(isinstance(report, dict) for report in reports) def test_get_all_reports_error(client, requests_mock): """Test error handling in get_all_reports""" requests_mock.get(f"{BASE_URL}api/v2/reports", status_code=500) reports = client.get_all_reports() assert len(reports) == 1 assert "Error getting reports" in reports[0]["msg"] def test_fetch_incidents_command_complete(client, requests_mock): """Test the complete fetch_incidents command flow""" # Mock successful token refresh mock_token_response = {"access_token": "new_token"} requests_mock.post("https://auth.cybelangel.com/oauth/token", json=mock_token_response) # Mock reports response mock_reports_response = { "reports": [ { "incident_id": "test-1", "created_at": "2024-02-19T10:00:00", "severity": 3, "category": "test", "abstract": "Test incident", } ] } requests_mock.get(f"{BASE_URL}api/v2/reports", json=mock_reports_response) # Test first fetch incidents = fetch_incidents(client, first_fetch=True, last_run=None, first_fetch_interval=1) assert len(incidents) == 1 assert incidents[0]["name"] == "CybelAngel Report - test-1" assert incidents[0]["severity"] == 3 assert incidents[0]["category"] == "test"