Halcyon
Halcyon is a device management platform that helps organizations monitor, control, and secure their network of devices. It provides centralized tools for overseeing hardware and software inventory, deploying updates, enforcing security policies, and ensuring compliance across device environments.
Analytics & SIEM · Halcyon
Details
| ID | Halcyon |
|---|---|
| Provider | Halcyon |
| Category | Analytics & SIEM |
| From Version | 8.2.0 |
| Docker Image | demisto/fastapi:0.125.0.10158186 |
README
Halcyon
Halcyon is a device management platform that helps organizations monitor, control, and secure their network of devices. It provides centralized tools for overseeing hardware and software inventory, deploying updates, enforcing security policies, and ensuring compliance across device environments.
This integration fetches alerts and events from the Halcyon platform and ingests them into Cortex XSIAM.
Configure Halcyon on Cortex XSIAM
- Navigate to Settings > Configurations > Data Collection > Automation & Feed Integrations.
- Search for Halcyon.
- Click Add instance to create and configure a new integration instance.
| Parameter | Description | Required |
|---|---|---|
| Server URL | The Halcyon API server URL. Default: https://api.halcyon.ai | True |
| Username | Username associated with your Halcyon account. | True |
| Password | Password associated with your Halcyon account. | True |
| Trust any certificate (not secure) | When selected, certificates are not checked. | False |
| Use system proxy settings | Runs the integration instance using the proxy server configured for the server. | False |
| Log types to fetch | Select which log types to fetch from Halcyon. Options: Alerts, Events. Default: Both. | True |
| Fetch events | When selected, the integration will fetch events. | False |
| Maximum number of alerts per fetch | Maximum number of alerts to fetch per fetch cycle. Default: 1000. | False |
| Maximum number of events per fetch | Maximum number of events to fetch per fetch cycle. Default: 1000. | False |
| Events Fetch Interval | How often to fetch events (in minutes). Default: 1 minute. | False |
- Click Test to validate the connection.
Commands
You can execute these commands from the Cortex XSIAM CLI, as part of an automation, or in a playbook.
halcyon-get-events
This command is used for developing/debugging and is to be used with caution, as it can create events, leading to events duplication and API request limitation exceeding.
Base Command
halcyon-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| limit | The maximum number of events to return. Default is 1000. | Optional |
| start_time | Filter events that occurred after this time. Supports ISO 8601 format or relative time expressions (e.g., “3 days ago”, “2024-01-01T00:00:00Z”). | Optional |
| end_time | Filter events that occurred before this time. Supports ISO 8601 format or relative time expressions (e.g., “now”, “2024-01-01T00:00:00Z”). | Optional |
| should_push_events | If true, the command creates events in XSIAM; otherwise, it only displays them. Possible values are: true, false. Default is false. | Optional |
Context Output
There is no context output for this command.
Human Readable Output
The command returns a table with the fetched events.
Fetched Event Types
The integration fetches the following event types:
Alerts
- API Endpoint:
/v2/alerts - Time Field:
lastOccurredAt - Source Log Type:
alerts
Alerts represent security-related notifications from the Halcyon platform, including threat detections, policy violations, and other security events.
Events
- API Endpoint:
/v2/events - Time Field:
occurredAt - Source Log Type:
events
Events represent general activity logs from the Halcyon platform, including device activities, system events, and operational logs.
Dataset
The data is stored in the halcyon_halcyon_raw dataset in Cortex XSIAM.
Troubleshooting
Authentication Issues
If you encounter authentication errors:
- Verify that the username and password are correct.
- Ensure the account has the necessary permissions to access the Halcyon API.
- Check if the account is locked or disabled.
Rate Limiting
If you encounter rate limiting errors:
- Reduce the maximum number of events per fetch.
- Increase the fetch interval.
- Contact Halcyon support if the issue persists.
Connection Issues
If you encounter connection errors:
- Verify the Server URL is correct.
- Check network connectivity to the Halcyon API.
- If using a proxy, ensure the proxy settings are configured correctly.
- If SSL certificate errors occur, you may need to enable “Trust any certificate” (not recommended for production).
Missing Events
If events are not being fetched:
- Verify that the “Fetch events” checkbox is enabled.
- Check that the correct log types are selected.
- Ensure the account has permissions to access the selected log types.
- Review the integration logs for any error messages.
Configuration parameters
url— Server URL (required)credentials— Username (required)tenant_id— Tenant ID (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsevent_types_to_fetch— Log types to fetch (required)isFetchEvents— Fetch eventsmax_fetch— Maximum number of events per fetcheventFetchInterval— Events Fetch Interval
Commands (1)
-
halcyon-get-eventsThis command is used for developing/debugging and is to be used with caution, as it can create events, leading to events duplication and API request limitation exceeding.
"""Unit tests for Halcyon integration.""" import json import pytest from freezegun import freeze_time def util_load_json(path: str) -> dict: """Load a JSON file for testing.""" with open(path, encoding="utf-8") as f: return json.load(f) @pytest.fixture def mock_client(mocker): """Create a mock client for testing.""" from Halcyon import Client, HalcyonAuthHandler # Mock the auth handler to avoid actual authentication mocker.patch.object(HalcyonAuthHandler, "_load_tokens_from_context") mocker.patch.object(HalcyonAuthHandler, "_save_tokens_to_context") # Mock ContentClient initialization mocker.patch("Halcyon.ContentClient.__init__", return_value=None) client = Client( base_url="https://api.halcyon.ai", username="test_user", password="test_password", tenant_id="test-tenant-id", verify=False, proxy=False, max_fetch=1000, ) # Set required attributes that would be set by ContentClient.__init__ client._base_url = "https://api.halcyon.ai" client._verify = False client.max_fetch = 1000 return client class TestHalcyonAuthHandler: """Test cases for the HalcyonAuthHandler class.""" def test_auth_handler_requires_username(self, mocker): """Test that auth handler requires a username.""" from Halcyon import HalcyonAuthHandler from ContentClientApiModule import ContentClientAuthenticationError mocker.patch("Halcyon.ContentClientContextStore") with pytest.raises(ContentClientAuthenticationError, match="non-empty username"): HalcyonAuthHandler(username="", password="test_password", tenant_id="test-tenant-id") def test_auth_handler_requires_password(self, mocker): """Test that auth handler requires a password.""" from Halcyon import HalcyonAuthHandler from ContentClientApiModule import ContentClientAuthenticationError mocker.patch("Halcyon.ContentClientContextStore") with pytest.raises(ContentClientAuthenticationError, match="non-empty password"): HalcyonAuthHandler(username="test_user", password="", tenant_id="test-tenant-id") class TestClient: """Test cases for the Client class.""" def test_get_alerts(self, mock_client, mocker): """Test fetching alerts.""" mock_response = { "data": [ { "alertId": "alert-1", "firstOccurredAt": "2024-01-01T00:00:00.000Z", "kind": "ThreatDetection", }, { "alertId": "alert-2", "firstOccurredAt": "2024-01-01T01:00:00.000Z", "kind": "PolicyViolation", }, ] } mocker.patch.object(mock_client, "_http_request", return_value=mock_response) result = mock_client.get_alerts( last_seen_after="2024-01-01T00:00:00.000Z", page=1, page_size=100, ) assert len(result["data"]) == 2 assert result["data"][0]["alertId"] == "alert-1" def test_get_events(self, mock_client, mocker): """Test fetching events.""" mock_response = { "data": [ { "eventId": "event-1", "occurredAt": "2024-01-01T00:00:00.000Z", "kind": "DeviceActivity", }, { "eventId": "event-2", "occurredAt": "2024-01-01T01:00:00.000Z", "kind": "SystemEvent", }, ] } mocker.patch.object(mock_client, "_http_request", return_value=mock_response) result = mock_client.get_events( occurred_after="2024-01-01T00:00:00.000Z", page=1, page_size=100, ) assert len(result["data"]) == 2 assert result["data"][0]["eventId"] == "event-1" class TestHelperFunctions: """Test cases for helper functions.""" def test_get_log_types_from_titles_valid(self): """Test getting log types from valid titles.""" from Halcyon import get_log_types_from_titles, LogType result = get_log_types_from_titles(["Alerts", "Events"]) assert len(result) == 2 assert LogType.ALERTS in result assert LogType.EVENTS in result def test_get_log_types_from_titles_single(self): """Test getting a single log type.""" from Halcyon import get_log_types_from_titles, LogType result = get_log_types_from_titles(["Alerts"]) assert len(result) == 1 assert LogType.ALERTS in result def test_get_log_types_from_titles_invalid(self): """Test getting log types from invalid titles.""" from Halcyon import get_log_types_from_titles from CommonServerPython import DemistoException with pytest.raises(DemistoException) as exc_info: get_log_types_from_titles(["InvalidType"]) assert "Invalid event type(s) provided" in str(exc_info.value) def test_enrich_events_alerts(self): """Test enriching alert events.""" from Halcyon import enrich_events, LogType events = [ { "alertId": "alert-1", "firstOccurredAt": "2024-01-01T00:00:00.000Z", } ] result = enrich_events(events, LogType.ALERTS) assert result[0]["_time"] == "2024-01-01T00:00:00.000Z" assert result[0]["source_log_type"] == "alerts" def test_enrich_events_events(self): """Test enriching event events.""" from Halcyon import enrich_events, LogType events = [ { "eventId": "event-1", "occurredAt": "2024-01-01T00:00:00.000Z", } ] result = enrich_events(events, LogType.EVENTS) assert result[0]["_time"] == "2024-01-01T00:00:00.000Z" assert result[0]["source_log_type"] == "events" def test_enrich_events_missing_time_field(self): """Test enriching events with missing time field.""" from Halcyon import enrich_events, LogType events = [ { "alertId": "alert-1", } ] result = enrich_events(events, LogType.ALERTS) assert "_time" not in result[0] assert result[0]["source_log_type"] == "alerts" def test_deduplicate_events(self): """Test deduplicating events.""" from Halcyon import deduplicate_events, LogType events = [ {"alertId": "alert-1", "firstOccurredAt": "2024-01-01T00:00:00.000Z"}, {"alertId": "alert-2", "firstOccurredAt": "2024-01-01T01:00:00.000Z"}, {"alertId": "alert-3", "firstOccurredAt": "2024-01-01T02:00:00.000Z"}, ] previous_run_ids = {"alert-1"} unique_events, new_ids, last_timestamp = deduplicate_events( events=events, previous_run_ids=previous_run_ids, previous_timestamp="2024-01-01T00:00:00.000Z", log_type=LogType.ALERTS, ) assert len(unique_events) == 2 assert "alert-2" in new_ids or "alert-3" in new_ids assert last_timestamp == "2024-01-01T02:00:00.000Z" def test_deduplicate_events_all_new(self): """Test deduplicating when all events are new.""" from Halcyon import deduplicate_events, LogType events = [ {"alertId": "alert-1", "firstOccurredAt": "2024-01-01T00:00:00.000Z"}, {"alertId": "alert-2", "firstOccurredAt": "2024-01-01T01:00:00.000Z"}, ] previous_run_ids: set = set() unique_events, new_ids, last_timestamp = deduplicate_events( events=events, previous_run_ids=previous_run_ids, previous_timestamp=None, log_type=LogType.ALERTS, ) assert len(unique_events) == 2 assert len(new_ids) == 1 # Only IDs sharing the last timestamp def test_deduplicate_events_all_duplicates(self): """Test deduplicating when all events are duplicates.""" from Halcyon import deduplicate_events, LogType events = [ {"alertId": "alert-1", "firstOccurredAt": "2024-01-01T00:00:00.000Z"}, {"alertId": "alert-2", "firstOccurredAt": "2024-01-01T00:00:00.000Z"}, ] previous_run_ids = {"alert-1", "alert-2"} unique_events, new_ids, last_timestamp = deduplicate_events( events=events, previous_run_ids=previous_run_ids, previous_timestamp="2024-01-01T00:00:00.000Z", log_type=LogType.ALERTS, ) assert len(unique_events) == 0 assert len(new_ids) == 0 assert last_timestamp is None class TestCommands: """Test cases for command functions.""" def test_test_module_success(self, mock_client, mocker): """Test successful test-module command.""" from Halcyon import test_module mocker.patch.object(mock_client, "get_alerts", return_value={"data": []}) result = test_module(mock_client) assert result == "ok" def test_test_module_failure(self, mock_client, mocker): """Test failed test-module command.""" from Halcyon import test_module from CommonServerPython import DemistoException mocker.patch.object(mock_client, "get_alerts", side_effect=Exception("Connection failed")) mocker.patch.object(mock_client, "diagnose_error", return_value={"issue": "Connection error"}) with pytest.raises(DemistoException) as exc_info: test_module(mock_client) assert "Test failed" in str(exc_info.value) def test_get_events_command(self, mock_client, mocker): """Test get-events command.""" from Halcyon import get_events_command mock_alerts_response = { "data": [ {"alertId": "alert-1", "firstOccurredAt": "2024-01-01T00:00:00.000Z"}, ] } mock_events_response = { "data": [ {"eventId": "event-1", "occurredAt": "2024-01-01T00:00:00.000Z"}, ] } def mock_get_alerts(*args, **kwargs): return mock_alerts_response def mock_get_events(*args, **kwargs): return mock_events_response mocker.patch.object(mock_client, "get_alerts", side_effect=mock_get_alerts) mocker.patch.object(mock_client, "get_events", side_effect=mock_get_events) events, results = get_events_command( client=mock_client, args={"limit": "10", "event_type": "Alerts,Events"}, event_types_to_fetch=["Alerts", "Events"], ) assert len(events) == 2 assert any(e.get("alertId") == "alert-1" for e in events) assert any(e.get("eventId") == "event-1" for e in events) def test_get_events_command_with_time_args(self, mock_client, mocker): """Test get-events command with time arguments.""" from Halcyon import get_events_command mock_response = {"data": []} mocker.patch.object(mock_client, "get_alerts", return_value=mock_response) mocker.patch.object(mock_client, "get_events", return_value=mock_response) events, results = get_events_command( client=mock_client, args={ "limit": "10", "start_time": "2024-01-01T00:00:00Z", "end_time": "2024-01-02T00:00:00Z", "event_type": "Alerts", }, event_types_to_fetch=["Alerts"], ) assert len(events) == 0 @freeze_time("2024-01-01T12:00:00Z") def test_fetch_events_command(self, mock_client, mocker): """Test fetch-events command.""" from Halcyon import fetch_events_command, LogType mock_alerts_response = { "data": [ {"alertId": "alert-1", "firstOccurredAt": "2024-01-01T10:00:00.000Z"}, ] } mock_events_response = { "data": [ {"eventId": "event-1", "occurredAt": "2024-01-01T11:00:00.000Z"}, ] } def mock_get_alerts(*args, **kwargs): return mock_alerts_response def mock_get_events(*args, **kwargs): return mock_events_response mocker.patch.object(mock_client, "get_alerts", side_effect=mock_get_alerts) mocker.patch.object(mock_client, "get_events", side_effect=mock_get_events) last_run = {} events, next_run = fetch_events_command( client=mock_client, last_run=last_run, log_types=[LogType.ALERTS, LogType.EVENTS], max_fetch=1000, ) assert len(events) == 2 assert "last_fetch_alerts" in next_run assert "last_fetch_events" in next_run @freeze_time("2024-01-01T12:00:00Z") def test_fetch_events_command_with_last_run(self, mock_client, mocker): """Test fetch-events command with existing last run.""" from Halcyon import fetch_events_command, LogType mock_alerts_response = { "data": [ {"alertId": "alert-2", "firstOccurredAt": "2024-01-01T11:00:00.000Z"}, ] } mock_events_response = {"data": []} mocker.patch.object(mock_client, "get_alerts", return_value=mock_alerts_response) mocker.patch.object(mock_client, "get_events", return_value=mock_events_response) last_run = { "last_fetch_alerts": "2024-01-01T10:00:00.000Z", "previous_ids_alerts": ["alert-1"], "last_fetch_events": "2024-01-01T10:00:00.000Z", "previous_ids_events": [], } events, next_run = fetch_events_command( client=mock_client, last_run=last_run, log_types=[LogType.ALERTS, LogType.EVENTS], max_fetch=1000, ) assert len(events) == 1 assert events[0]["alertId"] == "alert-2" @freeze_time("2024-01-01T12:00:00Z") def test_fetch_events_command_no_events(self, mock_client, mocker): """Test fetch-events command when no events are returned.""" from Halcyon import fetch_events_command, LogType mock_response = {"data": []} mocker.patch.object(mock_client, "get_alerts", return_value=mock_response) mocker.patch.object(mock_client, "get_events", return_value=mock_response) last_run = {} events, next_run = fetch_events_command( client=mock_client, last_run=last_run, log_types=[LogType.ALERTS, LogType.EVENTS], max_fetch=1000, ) assert len(events) == 0 # Should still set last_fetch times assert "last_fetch_alerts" in next_run assert "last_fetch_events" in next_run class TestFetchEventsForLogType: """Test cases for fetch_events_for_log_type function.""" @freeze_time("2024-01-01T12:00:00Z") def test_fetch_with_pagination(self, mock_client, mocker): """Test fetching events with pagination.""" from Halcyon import fetch_events_for_log_type, LogType # First page returns full page, second page returns partial page1_response = { "data": [{"alertId": f"alert-{i}", "firstOccurredAt": f"2024-01-01T{i:02d}:00:00.000Z"} for i in range(100)] } page2_response = { "data": [{"alertId": f"alert-{i}", "firstOccurredAt": f"2024-01-01T{i:02d}:00:00.000Z"} for i in range(100, 150)] } call_count = 0 def mock_get_alerts(*args, **kwargs): nonlocal call_count call_count += 1 if call_count == 1: return page1_response return page2_response mocker.patch.object(mock_client, "get_alerts", side_effect=mock_get_alerts) events, last_run = fetch_events_for_log_type( client=mock_client, log_type=LogType.ALERTS, last_run={}, max_fetch=200, ) assert len(events) == 150 assert call_count == 2 @freeze_time("2024-01-01T12:00:00Z") def test_fetch_respects_max_fetch(self, mock_client, mocker): """Test that fetching respects max_fetch limit.""" from Halcyon import fetch_events_for_log_type, LogType # Return more events than max_fetch response = {"data": [{"alertId": f"alert-{i}", "firstOccurredAt": f"2024-01-01T{i:02d}:00:00.000Z"} for i in range(100)]} mocker.patch.object(mock_client, "get_alerts", return_value=response) events, last_run = fetch_events_for_log_type( client=mock_client, log_type=LogType.ALERTS, last_run={}, max_fetch=50, ) assert len(events) == 50 class TestLogTypeEnum: """Test cases for LogType enum.""" def test_alerts_log_type(self): """Test ALERTS log type properties.""" from Halcyon import LogType assert LogType.ALERTS.type_string == "alerts" assert LogType.ALERTS.title == "Alerts" assert LogType.ALERTS.api_endpoint == "/v2/alerts" assert LogType.ALERTS.time_field == "firstOccurredAt" def test_events_log_type(self): """Test EVENTS log type properties.""" from Halcyon import LogType assert LogType.EVENTS.type_string == "events" assert LogType.EVENTS.title == "Events" assert LogType.EVENTS.api_endpoint == "/v2/events" assert LogType.EVENTS.time_field == "occurredAt"