VercaraUltraDNS
Vercara UltraDNS integration for Cortex. Leverage UltraDNS's cloud-based DNS event and configuration data for security automation and real-time threat detection in Cortex.
Analytics & SIEM · Vercara UltraDNS
Details
| ID | VercaraUltraDNS |
|---|---|
| Provider | DigiCert |
| Category | Analytics & SIEM |
| From Version | 8.4.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | XSIAM |
README
This integration fetches DNS configuration audit logs from Vercara UltraDNS platform.
This integration was integrated and tested with the 3.18.0 Vercara UltraDNS API.
This is the default integration for this content pack when configured by the Data Onboarder in Cortex XSIAM.
Configure Vercara UltraDNS in Cortex
| Parameter | Description | Required |
|---|---|---|
| Server URL | The base URL for the Vercara UltraDNS API. Default is https://api.ultradns.com | True |
| Username | Username for authentication | True |
| Password | Password for authentication | True |
| Trust any certificate (not secure) | Use SSL secure connection or not. | False |
| Use system proxy settings | Use proxy settings for connection or not. | False |
| Fetch events | Whether to bring events or not. | False |
| The maximum number of audit logs per fetch | Maximum number of events to fetch per cycle. Default is 2,500, maximum is 2,500. | False |
Vercara UltraDNS Authentication
The integration uses OAuth 2.0 password grant flow for authentication:
- Initial Token Request: Uses username/password to obtain access token and refresh token
- Token Usage: Access token is used in Authorization header for API requests
- Token Refresh: Automatically refreshes tokens when they expire, with fallback to username/password if refresh fails.
For detailed API documentation, see the REST API User Guide.
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.
vercara-ultradns-get-events
Gets audit events from Vercara UltraDNS. Manual command to fetch and display 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 exceeding the API request limitation.
Base Command
vercara-ultradns-get-events
Input
| Argument Name | Description | Required |
|---|---|---|
| should_push_events | If true, the command creates events; otherwise, it only displays them. Possible values are true and false. The default value is false. | Required |
| limit | Maximum number of events to return. Default is 50, maximum is 2,500. | Optional |
| start_time | Start time for event collection. Supports ISO format (“2023-01-01T00:00:00”) or natural language (“7 days ago”, “yesterday”, “1 week ago”). | Required |
| end_time | End time for event collection. Supports ISO format (“2023-01-01T23:59:59”) or natural language (“2 hours ago”, “now”). Default is now if not provided. | Optional |
Context Output
There is no context output for this command.
Configuration parameters
url— Server URL (required)credentials— Username (required)isFetchEvents— Fetch eventsmax_events_per_fetch— The maximum number of audit logs per fetchinsecure— Trust any certificate (not secure)proxy— Use system proxy settings
Commands (1)
-
vercara-ultradns-get-eventsGets audit events from Vercara UltraDNS. Manual command to fetch and display 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 exceeding the API request limitation.
import pytest import requests_mock from freezegun import freeze_time from datetime import datetime import json from unittest.mock import patch from CommonServerPython import * from VercaraUltraDNS import ( Client, convert_time_string, _calculate_event_hash, _deduplicate_events, _cache_recent_events, _cleanup_event_cache, process_events_for_xsiam, fetch_events, get_events_command, VENDOR, PRODUCT, ) BASE_URL = "https://example.ultradns.com/v2" USERNAME = "test_user" PASSWORD = "test_password" TOKEN_URL = f"{BASE_URL}/authorization/token" AUDIT_LOG_URL = f"{BASE_URL}/reports/dns_configuration/audit" """ HELPER FUNCTIONS """ def util_load_json(path): """Load JSON data from test_data directory.""" with open(path, encoding="utf-8") as f: return json.loads(f.read()) def mock_send_events_to_xsiam(events, vendor, product): """Mock function for send_events_to_xsiam.""" return events, vendor, product @pytest.fixture def client() -> Client: """ Fixture to create and return a Client instance for testing. Uses mock credentials defined at the top of the file. """ return Client(base_url=BASE_URL, username=USERNAME, password=PASSWORD, verify=True, proxy=False) """ TEST FUNCTIONS """ class TestClient: """Test cases for Client class.""" def test_client_init(self): """ Given: - Valid UltraDNS API credentials and base URL When: - Creating a new Client instance Then: - Client should be initialized with correct parameters - OAuth token should be empty initially """ client = Client(base_url=BASE_URL, username=USERNAME, password=PASSWORD, verify=True, proxy=False) assert client._base_url == BASE_URL assert client.username == USERNAME assert client.password == PASSWORD assert client.access_token is None def test_authenticate_success(self): """ Given: - Valid UltraDNS API credentials - Mock OAuth token endpoint returning valid token When: - Calling get_access_token method Then: - Should successfully obtain and store OAuth token - Token expiry should be calculated correctly """ token_response = util_load_json("test_data/oauth_token_response.json") with requests_mock.Mocker() as mocker: mocker.post(TOKEN_URL, json=token_response) client = Client(BASE_URL, USERNAME, PASSWORD) with freeze_time("2025-09-27 14:30:00"): access_token = client.get_access_token() assert access_token == "abc_test" assert client.token_expires_in == 3500 def test_authenticate_failure(self): """ Given: - Invalid UltraDNS API credentials - Mock OAuth token endpoint returning 401 error When: - Calling get_access_token method Then: - Should raise DemistoException with authentication error """ with requests_mock.Mocker() as mocker: mocker.post(TOKEN_URL, status_code=401, json={"error": "invalid_credentials"}) client = Client(BASE_URL, USERNAME, PASSWORD) with pytest.raises(DemistoException): client.get_access_token() def test_get_audit_logs_success(self): """ Given: - Authenticated client - Mock audit logs endpoint returning events - Valid time range parameters When: - Calling get_audit_logs method Then: - Should return audit events from API - Should include pagination cursor if available """ token_response = util_load_json("test_data/oauth_token_response.json") audit_response = util_load_json("test_data/audit_events.json") with requests_mock.Mocker() as mocker: mocker.post(TOKEN_URL, json=token_response) mocker.get(AUDIT_LOG_URL, json=audit_response) client = Client(BASE_URL, USERNAME, PASSWORD) start_time = datetime(2025, 9, 27, 14, 0, 0) end_time = datetime(2025, 9, 27, 15, 0, 0) result = client.get_audit_logs(start_time=start_time, end_time=end_time, limit=100) assert "auditRecords" in result assert len(result["auditRecords"]) == 3 assert result["next_cursor"] == "cursor_123" class TestUtilityFunctions: """Test cases for utility functions.""" def test_convert_time_string_success(self): """ Given: - Valid API time string in expected format When: - Converting time string to datetime object Then: - Should return correct timezone-naive datetime object """ time_str = "2025-09-27 14:30:00.0" expected = datetime(2025, 9, 27, 14, 30, 0) result = convert_time_string(time_str) assert isinstance(result, datetime) assert result.tzinfo is None # Should be timezone-naive assert result == expected def test_convert_time_string_invalid(self): """ Given: - Invalid time string that cannot be parsed with API format When: - Converting time string to datetime object Then: - Should raise DemistoException with detailed parsing error """ invalid_time = "2025-09-27T14:30:00" # Wrong format (ISO instead of API) with pytest.raises(DemistoException, match="Failed to parse time string"): convert_time_string(invalid_time) def test_calculate_event_hash(self): """ Given: - Event dictionary with various fields When: - Calculating hash for the event Then: - Should return consistent SHA256 hash - Same event should produce same hash - Different events should produce different hashes """ event1 = {"changeTime": "2025-09-27 14:30:00.0", "user": "admin", "object": "test.com"} event2 = { "object": "test.com", # Different order "user": "admin", "changeTime": "2025-09-27 14:30:00.0", } event3 = {"changeTime": "2025-09-27 14:30:00.0", "user": "admin", "object": "different.com"} hash1 = _calculate_event_hash(event1) hash2 = _calculate_event_hash(event2) hash3 = _calculate_event_hash(event3) assert hash1 == hash2 # Same content, different order should produce same hash assert hash1 != hash3 # Different content should produce different hash assert len(hash1) == 64 # SHA256 hex string length class TestDeduplication: """Test cases for event deduplication logic.""" def test_deduplicate_events_no_duplicates(self): """ Given: - List of unique events - Empty event cache - Upper bound for duplicate checking When: - Running deduplication Then: - Should return all events unchanged - No events should be filtered out """ events = [ {"changeTime": "2025-09-27 14:30:00.0", "user": "admin", "object": "test1.com"}, {"changeTime": "2025-09-27 14:29:00.0", "user": "admin", "object": "test2.com"}, ] event_cache = {} upper_bound = datetime(2025, 9, 27, 14, 30, 1) result = _deduplicate_events(events, event_cache, upper_bound) assert len(result) == 2 assert result == events def test_deduplicate_events_with_duplicates(self): """ Given: - List of events with some duplicates - Event cache containing hashes of previous events - Upper bound for duplicate checking When: - Running deduplication Then: - Should filter out duplicate events - Should keep unique events """ events = [ {"changeTime": "2025-09-27 14:30:00.0", "user": "admin", "object": "test1.com"}, {"changeTime": "2025-09-27 14:29:00.0", "user": "admin", "object": "test2.com"}, ] # Pre-populate cache with hash of second event duplicate_hash = _calculate_event_hash(events[1]) event_cache = {duplicate_hash: "2025-09-27T14:29:00"} upper_bound = datetime(2025, 9, 27, 14, 30, 1) result = _deduplicate_events(events, event_cache, upper_bound) assert len(result) == 1 assert result[0] == events[0] # First event should remain def test_deduplicate_events_boundary_optimization(self): """ Given: - List of events with some newer than upper boundary - Event cache containing hash of the "newer" event (simulating it was processed before) - Upper bound for duplicate checking When: - Running deduplication Then: - Should add events newer than boundary without duplicate check (ignoring cache) - Should check duplicates for events within boundary """ events = [ {"changeTime": "2025-09-27 14:35:00.0", "user": "admin", "object": "new.com"}, # Newer than boundary {"changeTime": "2025-09-27 14:30:00.0", "user": "admin", "object": "boundary.com"}, {"changeTime": "2025-09-27 14:25:00.0", "user": "admin", "object": "old.com"}, ] # Pre-populate cache with hash of the "new.com" event to test optimization new_event_hash = _calculate_event_hash(events[0]) old_event_hash = _calculate_event_hash(events[2]) event_cache = { new_event_hash: "2025-09-27T14:35:00", # This should be ignored due to optimization old_event_hash: "2025-09-27T14:25:00", # This should cause old.com to be filtered } upper_bound = datetime(2025, 9, 27, 14, 30, 1) # Between first and second event result = _deduplicate_events(events, event_cache, upper_bound) # Should have 2 events: new.com (bypassed cache) and boundary.com (not in cache) # old.com should be filtered out due to duplicate in cache assert len(result) == 2 assert result[0]["object"] == "new.com" # Newer event kept despite being in cache (for testing only, # in real life it will not be in cache) assert result[1]["object"] == "boundary.com" # Boundary event kept (not in cache) class TestCaching: """Test cases for event caching logic.""" def test_cache_recent_events(self): """ Given: - List of events with different timestamps relative to cutoff - Empty cache dictionary - Cutoff time for caching When: - Caching recent events Then: - Should cache events newer than or equal to cutoff - Should not cache events older than cutoff """ events = [ {"changeTime": "2025-09-27 14:30:00.0", "user": "admin", "object": "recent.com"}, # Newer {"changeTime": "2025-09-27 14:25:00.0", "user": "admin", "object": "exact.com"}, # Equal to cutoff {"changeTime": "2025-09-27 14:20:00.0", "user": "admin", "object": "old.com"}, # Older ] with patch("VercaraUltraDNS._calculate_event_hash") as mock_hash: mock_hash.side_effect = ["hash_recent", "hash_exact", "hash_old"] cache = {} cutoff_time = datetime(2025, 9, 27, 14, 25, 0) _cache_recent_events(events, cache, cutoff_time) # Should cache 2 events: newer + equal to cutoff (>= behavior) assert len(cache) == 2 assert "hash_recent" in cache assert "hash_exact" in cache # Equal to cutoff should be cached assert "hash_old" not in cache # Older than cutoff should not be cached assert cache["hash_recent"] == "2025-09-27T14:30:00" assert cache["hash_exact"] == "2025-09-27T14:25:00" # Verify hash function was called for the cached events only assert mock_hash.call_count == 2 # Only called for events that got cached def test_cleanup_event_cache(self): """ Given: - Event cache with old and recent entries - Cutoff time for cleanup When: - Cleaning up event cache Then: - Should remove entries older than cutoff - Should keep entries newer than cutoff """ event_cache = { "hash1": "2025-09-27T14:30:00", # Recent "hash2": "2025-09-27T14:20:00", # Old "hash3": "2025-09-27T14:35:00", # Recent } cutoff_time = datetime(2025, 9, 27, 14, 25, 0) result = _cleanup_event_cache(event_cache, cutoff_time) assert len(result) == 2 assert "hash1" in result assert "hash3" in result assert "hash2" not in result def test_cleanup_event_cache_invalid_timestamps(self): """ Given: - Event cache with some invalid timestamp entries - Cutoff time for cleanup When: - Cleaning up event cache Then: - Should handle invalid timestamps gracefully - Should keep valid entries """ event_cache = { "hash1": "2025-09-27T14:30:00", # Valid "hash2": "invalid_timestamp", # Invalid "hash3": "2025-09-27T14:35:00", # Valid } cutoff_time = datetime(2025, 9, 27, 14, 25, 0) result = _cleanup_event_cache(event_cache, cutoff_time) # Should keep valid entries assert "hash1" in result assert "hash3" in result class TestEventProcessing: """Test cases for event processing functions.""" def test_process_events_for_xsiam(self): """ Given: - List of events with changeTime field When: - Processing events for XSIAM ingestion Then: - Should add _time, _vendor, _product fields - Should convert changeTime to timestamp """ events = [ {"changeTime": "2025-09-27 14:30:00.0", "user": "admin", "object": "test.com"}, {"changeTime": "2025-09-27 14:31:00.0", "user": "admin", "object": "test2.com"}, ] result = process_events_for_xsiam(events) assert len(result) == 2 for event in result: assert "_time" in event assert "_vendor" in event assert "_product" in event assert event["_vendor"] == VENDOR assert event["_product"] == PRODUCT assert isinstance(event["_time"], int | float) class TestMainFunctions: """Test cases for main integration functions.""" def test_test_module_success(self): """ Given: - Valid UltraDNS API credentials - Mock API endpoints returning successful responses When: - Running test module Then: - Should return "ok" indicating successful connection """ token_response = util_load_json("test_data/oauth_token_response.json") audit_response = util_load_json("test_data/audit_events_empty.json") from VercaraUltraDNS import test_module with requests_mock.Mocker() as mocker: mocker.post(TOKEN_URL, json=token_response) mocker.get(AUDIT_LOG_URL, json=audit_response) client = Client(BASE_URL, USERNAME, PASSWORD) result = test_module(client, {}) assert result == "ok" def test_test_module_failure(self): """ Given: - Invalid UltraDNS API credentials - Mock API endpoints returning authentication errors When: - Running test module Then: - Should return error message with authentication failure """ from VercaraUltraDNS import test_module with requests_mock.Mocker() as mocker: mocker.post(TOKEN_URL, status_code=401, json={"error": "invalid_credentials"}) client = Client(BASE_URL, USERNAME, PASSWORD) result = test_module(client, {}) assert "Connection failed:" in result def test_get_events_command(self, mocker): """ Given: - Valid API client and parameters - Mock audit logs endpoint returning events When: - Running get-events command Then: - Should fetch events from API - Should return CommandResults with events - Should optionally push events to XSIAM """ token_response = util_load_json("test_data/oauth_token_response.json") audit_response = util_load_json("test_data/audit_events.json") # Remove next_cursor to simulate single page response audit_response.pop("next_cursor", None) mock_send = mocker.patch("VercaraUltraDNS.send_events_to_xsiam", side_effect=mock_send_events_to_xsiam) with requests_mock.Mocker() as requests_mocker: requests_mocker.post(TOKEN_URL, json=token_response) requests_mocker.get(AUDIT_LOG_URL, json=audit_response) client = Client(BASE_URL, USERNAME, PASSWORD) args = { "limit": "10", "start_time": "2025-09-27T14:00:00", "end_time": "2025-09-27T15:00:00", "should_push_events": "true", } events, result = get_events_command(client, args) assert isinstance(result, CommandResults) assert len(events) == 3 mock_send.assert_called_once() @freeze_time("2025-09-27 15:00:00") def test_fetch_events_first_run(self): """ Given: - Empty last_run state (first fetch) - Mock API returning events When: - Running fetch events Then: - Should fetch events from last 3 hours - Should return proper next_run state - Should return processed events """ token_response = util_load_json("test_data/oauth_token_response.json") audit_response = util_load_json("test_data/audit_events.json") # Remove next_cursor to simulate single page response audit_response.pop("next_cursor", None) with requests_mock.Mocker() as mocker: mocker.post(TOKEN_URL, json=token_response) mocker.get(AUDIT_LOG_URL, json=audit_response) client = Client(BASE_URL, USERNAME, PASSWORD) last_run = {} next_run, events = fetch_events(client=client, last_run=last_run, max_events_per_fetch=100) assert "last_fetch_time" in next_run assert "event_cache" in next_run assert len(events) == 3 assert all("_time" in event for event in events) assert all("_vendor" in event for event in events) @freeze_time("2025-09-27 15:00:00") def test_fetch_events_no_new_events(self): """ Given: - Previous last_run state - Mock API returning no new events When: - Running fetch events Then: - Should return unchanged last_run state - Should return empty events list """ token_response = util_load_json("test_data/oauth_token_response.json") audit_response = util_load_json("test_data/audit_events_empty.json") with requests_mock.Mocker() as mocker: mocker.post(TOKEN_URL, json=token_response) mocker.get(AUDIT_LOG_URL, json=audit_response) client = Client(BASE_URL, USERNAME, PASSWORD) last_run = {"last_fetch_time": "2025-09-27T14:25:00", "event_cache": {"test_hash": "2025-09-27T14:20:00"}} next_run, events = fetch_events(client=client, last_run=last_run, max_events_per_fetch=100) assert next_run == last_run # Should be unchanged assert len(events) == 0 @freeze_time("2025-09-27 15:00:00") def test_fetch_events_deduplication__end_to_end(self): """ Given: - Previous last_run with timestamp and cached event hashes - Mock API returning events including duplicates When: - Running fetch events Then: - Should use last_fetch_time - 3s for API call - Should deduplicate using cached hashes - Should update next_run with exact timestamp and cache values - Should clean up old cache entries """ token_response = util_load_json("test_data/oauth_token_response.json") test_events = util_load_json("test_data/audit_events.json") # Pre-calculate hash for last event to simulate it's already cached (duplicate) duplicate_event = test_events["auditRecords"][2] # 14:30:00 event (oldest) duplicate_hash = _calculate_event_hash(duplicate_event) # Pre-calculate hash for newest event for validation first_event = test_events["auditRecords"][0] # 14:32:30 event (newest) first_hash = _calculate_event_hash(first_event) # Remove next_cursor to simulate single page response test_events.pop("next_cursor", None) with requests_mock.Mocker() as mocker: mocker.post(TOKEN_URL, json=token_response) mocker.get(AUDIT_LOG_URL, json=test_events) client = Client(BASE_URL, USERNAME, PASSWORD) # Setup last_run with cached duplicate and an old hash that should be cleaned up old_hash = "old_hash_should_be_cleaned" last_run = { "last_fetch_time": "2025-09-27T14:30:05", # After the oldest event (14:30:00) "event_cache": { duplicate_hash: "2025-09-27T14:30:00", # Duplicate oldest event old_hash: "2025-09-27T14:20:00", # Old hash to be cleaned }, } next_run, events = fetch_events(client=client, last_run=last_run, max_events_per_fetch=100) # 1. Verify API called with correct lookback (last_fetch_time - 3 seconds) # 14:30:05 - 3 = 14:30:02 expected_filter = "date_range%3A20250927143002-20250927150000" assert f"filter={expected_filter}" in mocker.last_request.url # 2. Verify deduplication worked - oldest event filtered, 2 returned assert len(events) == 2 # 3. Verify next_run has exact timestamp of newest event assert next_run["last_fetch_time"] == "2025-09-27T14:32:30" # Newest event # 4. Verify exact cache contents # Cache cutoff = newest event time (14:32:30) - (3+1)s = 14:32:26 # Only events >= 14:32:26 should be cached expected_cache = { first_hash: "2025-09-27T14:32:30", # Newest event cached (14:32:30 >= 14:32:26) # second_hash NOT cached - 14:31:15 < 14:32:26 (before cutoff) # duplicate_hash (14:30:00) should be cleaned up (before cutoff) # old_hash should be cleaned up (not present) } assert next_run["event_cache"] == expected_cache # 5. Verify returned events are the correct ones (not the duplicate) returned_timestamps = [event["changeTime"] for event in events] expected_timestamps = ["2025-09-27 14:32:30.0", "2025-09-27 14:31:15.0"] # Oldest event (14:30:00) was filtered assert returned_timestamps == expected_timestamps