CybelAngel

This integration connects your alerts from CybelAngel.

Data Enrichment & Threat Intelligence · CybelAngel

Details

IDCybelAngel
ProviderCybelAngel
CategoryData Enrichment & Threat Intelligence
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix 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

  1. Navigate to Settings > Integrations > Servers & Services
  2. Search for CybelAngel
  3. Click Add instance
  4. 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 URL
  • max_fetch — Max Fetch
  • isFetch — Fetch incidents
  • incidentFetchInterval — Incidents Fetch Interval
  • incidentType — Incident type

Commands (7)

  • cybelangel-get-comments

    Get comments for a report.

  • cybelangel-get-report-attachment

    Get an attachment of a report.

  • cybelangel-get-report-by-id

    Get a report by ID.

  • cybelangel-get-report-pdf

    Downloads a PDF report from CybelAngel by report ID and saves it to the War Room.

  • cybelangel-post-comment

    Post a comment on a report.

  • cybelangel-remediate

    Remediate a report by ID.

  • cybelangel-update-status

    Update 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"