DatadogCloudSIEMV2
Datadog Cloud SIEM integration for XSOAR provides security signal management capabilities. This integration allows you to retrieve, filter, and manage security signals from Datadog's Cloud SIEM platform, enabling security teams to investigate threats, manage signal triage states, and assign signals to team members. Supports incoming mirroring of signals to XSOAR incidents.
Analytics & SIEM · Datadog Cloud SIEM
Details
| ID | DatadogCloudSIEMV2 |
|---|---|
| Provider | Datadog |
| Category | Analytics & SIEM |
| From Version | 6.8.0 |
| Docker Image | demisto/datadog-api-client:1.0.0.10133006 |
| Supported Modules | Agentix XSIAM |
README
Datadog Cloud SIEM
Datadog Cloud SIEM integration for Cortex XSOAR provides security signal management and log search capabilities for threat detection and incident response.
This integration allows security teams to:
- Fetch security signals as XSOAR incidents automatically
- Retrieve and filter security signals from Datadog’s Cloud SIEM platform
- Manage signal triage states and assignments
- Search security logs for investigations
- Extract IOCs (IPs, URLs, file hashes) from security signals
This integration was integrated and tested with version 2.12.0 of datadog-api-client.
Configure Datadog Cloud SIEM on Cortex XSOAR
- Navigate to Settings > Integrations > Instances.
- Search for Datadog Cloud SIEM.
- Click Add instance to create and configure a new integration instance.
| Parameter | Description | Required |
|---|---|---|
| Server URL | Datadog website URL | True |
| API Key | The API Key to use for authentication | True |
| Application Key | The APP Key to use for authentication | True |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False | |
| Fetch incidents | Enable fetching security signals as incidents | False |
| Incident type | The incident type to assign to fetched incidents | False |
| First fetch time | First fetch time range. How far back to fetch security signals on the first run. Examples “3 days”, “7 days”, “1 week” | False |
| Max fetch | Maximum number of incidents to fetch per cycle. Maximum is 200, recommended less than 100. | False |
| Fetch severity | Fetch only security signals with these severity levels or higher. | False |
| Fetch state | Fetch only security signals in this state. Default is “open”. | False |
| Fetch query | Additional custom query filter for fetching security signals. Uses Datadog search syntax. | False |
| Incident Mirroring Direction | Choose the mirroring direction for incidents. Incoming mirrors changes from Datadog to XSOAR. Cortex XSOAR only parameter. | False |
| Close Mirrored XSOAR Incident | When selected, archiving the Datadog security signal is mirrored in XSOAR and closes the XSOAR incident. Cortex XSOAR only parameter. | False |
- Click Test to validate the URLs, keys, and connection.
Commands
You can execute these commands from the Cortex XSOAR 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.
datadog-get-signal
Get a specific security signal by ID from Datadog Cloud SIEM.
Base Command
datadog-get-signal
Input
| Argument Name | Description | Required |
|---|---|---|
| signal_id | The unique identifier of the security signal to retrieve. If not provided, will attempt to get it from the current incident. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Datadog.SecuritySignal.id | String | The unique identifier of the security signal. |
| Datadog.SecuritySignal.event_id | String | The event ID of the security signal. |
| Datadog.SecuritySignal.timestamp | String | The timestamp when the security signal was generated. |
| Datadog.SecuritySignal.host | String | Host associated with the security signal. |
| Datadog.SecuritySignal.service | String | Services associated with the security signal. |
| Datadog.SecuritySignal.severity | String | The severity level of the security signal (info, low, medium, high, critical). |
| Datadog.SecuritySignal.title | String | The title of the security signal. |
| Datadog.SecuritySignal.message | String | The message describing the security signal. |
| Datadog.SecuritySignal.tags | Unknown | List of tags associated with the security signal. |
| Datadog.SecuritySignal.triggering_log_id | String | ID of the log that triggered the security signal. |
| Datadog.SecuritySignal.url | String | URL to view the security signal in Datadog UI. |
| Datadog.SecuritySignal.rule.id | String | The unique identifier of the security rule. |
| Datadog.SecuritySignal.rule.url | String | URL to view the security rule in Datadog UI. |
| Datadog.SecuritySignal.triage.state | String | The current triage state of the security signal (open, under_review, archived). |
| Datadog.SecuritySignal.triage.archive_comment | String | The archive comment of the security signal. |
| Datadog.SecuritySignal.triage.archive_reason | String | The archive reason of the security signal. |
| Datadog.SecuritySignal.triage.assignee.name | String | The name of the user assigned to the security signal. |
| Datadog.SecuritySignal.triage.assignee.handle | String | The handle of the user assigned to the security signal. |
| Datadog.SecuritySignal.raw | Unknown | The raw signal object returned by the API. |
Command example
!datadog-get-signal signal_id=AZm-wsEuAACEnzdcj-YEigAA
!datadog-get-signal
datadog-list-signals
Get a list of security signals from Datadog Cloud SIEM with optional filtering and pagination.
Base Command
datadog-list-signals
Input
| Argument Name | Description | Required |
|---|---|---|
| state | Filter signals by state. Possible values are: open, under_review, archived. | Optional |
| severity | Filter signals by severity level. Possible values are: info, low, medium, high, critical. | Optional |
| source | Filter signals by source. | Optional |
| query | Custom query string for advanced filtering. Uses Datadog search syntax. | Optional |
| from_date | Start date for the search. Format can be relative (e.g., “7 days ago”) or absolute (e.g., “2023-01-01T00:00:00Z”). Default: -7days. | Optional |
| to_date | End date for the search. Format can be relative (e.g., “now”) or absolute (e.g., “2023-01-01T23:59:59Z”). Default: now. | Optional |
| sort | Sort order for results. Possible values are: asc, desc. Default: desc. | Optional |
| page_size | Number of results per page. | Optional |
| limit | Maximum number of results to return. If page_size is specified, limit is ignored. Default: 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Datadog.SecuritySignal.id | String | The unique identifier of the security signal. |
| Datadog.SecuritySignal.event_id | String | The event ID of the security signal. |
| Datadog.SecuritySignal.timestamp | String | The timestamp when the security signal was generated. |
| Datadog.SecuritySignal.host | String | Host associated with the security signal. |
| Datadog.SecuritySignal.service | String | Services associated with the security signal. |
| Datadog.SecuritySignal.severity | String | The severity level of the security signal (info, low, medium, high, critical). |
| Datadog.SecuritySignal.title | String | The title of the security signal. |
| Datadog.SecuritySignal.message | String | The message describing the security signal. |
| Datadog.SecuritySignal.tags | Unknown | List of tags associated with the security signal. |
| Datadog.SecuritySignal.triggering_log_id | String | ID of the log that triggered the security signal. |
| Datadog.SecuritySignal.url | String | URL to view the security signal in Datadog UI. |
| Datadog.SecuritySignal.rule.id | String | The unique identifier of the security rule. |
| Datadog.SecuritySignal.rule.url | String | URL to view the security rule in Datadog UI. |
| Datadog.SecuritySignal.triage.state | String | The current triage state of the security signal (open, under_review, archived). |
| Datadog.SecuritySignal.triage.archive_comment | String | The archive comment of the security signal. |
| Datadog.SecuritySignal.triage.archive_reason | String | The archive reason of the security signal. |
| Datadog.SecuritySignal.triage.assignee.name | String | The name of the user assigned to the security signal. |
| Datadog.SecuritySignal.triage.assignee.handle | String | The handle of the user assigned to the security signal. |
| Datadog.SecuritySignal.raw | Unknown | The raw signal object returned by the API. |
Command example
!datadog-list-signals state=open severity=high
!datadog-list-signals from_date="-3days" to_date="now" limit=10
datadog-update-signal-state
Update a security signal’s assignee and/or state in Datadog Cloud SIEM. Can update assignee only, state only, or both in a single command.
Base Command
datadog-update-signal-state
Input
| Argument Name | Description | Required |
|---|---|---|
| signal_id | The unique identifier of the security signal to update. If not provided, will attempt to get it from the current incident. | Optional |
| assignee | Name or email of the user to assign to the security signal. Leave empty to unassign. At least one of assignee or state must be provided. | Optional |
| state | The new state of the security signal. Possible values are: open, under_review, archived. At least one of assignee or state must be provided. | Optional |
| archive_reason | Reason for the state change (used when changing state to archived). Possible values are: none, false_positive, testing_or_maintenance, remediated, investigated_case_opened, other, true_positive_benign, true_positive_malicious. | Optional |
| archive_comment | Comment about the state change (used when changing state to archived). | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Datadog.SecuritySignal.id | String | The unique identifier of the security signal. |
| Datadog.SecuritySignal.event_id | String | The event ID of the security signal. |
| Datadog.SecuritySignal.timestamp | String | The timestamp when the security signal was generated. |
| Datadog.SecuritySignal.host | String | Host associated with the security signal. |
| Datadog.SecuritySignal.service | String | Services associated with the security signal. |
| Datadog.SecuritySignal.severity | String | The severity level of the security signal (info, low, medium, high, critical). |
| Datadog.SecuritySignal.title | String | The title of the security signal. |
| Datadog.SecuritySignal.message | String | The message describing the security signal. |
| Datadog.SecuritySignal.tags | Unknown | List of tags associated with the security signal. |
| Datadog.SecuritySignal.triggering_log_id | String | ID of the log that triggered the security signal. |
| Datadog.SecuritySignal.url | String | URL to view the security signal in Datadog UI. |
| Datadog.SecuritySignal.rule.id | String | The unique identifier of the security rule. |
| Datadog.SecuritySignal.rule.url | String | URL to view the security rule in Datadog UI. |
| Datadog.SecuritySignal.triage.state | String | The current triage state of the security signal (open, under_review, archived). |
| Datadog.SecuritySignal.triage.archive_comment | String | The archive comment of the security signal. |
| Datadog.SecuritySignal.triage.archive_reason | String | The archive reason of the security signal. |
| Datadog.SecuritySignal.triage.assignee.name | String | The name of the user assigned to the security signal. |
| Datadog.SecuritySignal.triage.assignee.handle | String | The handle of the user assigned to the security signal. |
| Datadog.SecuritySignal.raw | Unknown | The raw signal object returned by the API. |
Command example
!datadog-update-signal-state signal_id=AZm-wsEuAACEnzdcj-YEigAA state=archived archive_reason=false_positive archive_comment="Not a real threat"
!datadog-update-signal-state assignee=user@example.com
!datadog-update-signal-state state=under_review
datadog-add-signal-comment
Add a comment to a security signal in Datadog Cloud SIEM.
Base Command
datadog-add-signal-comment
Input
| Argument Name | Description | Required |
|---|---|---|
| event_id | The event ID of the security signal to add a comment to. If not provided, will attempt to get it from the current incident. | Optional |
| comment | The comment text to add to the security signal. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Datadog.SecurityComment.id | String | The unique identifier of the comment. |
| Datadog.SecurityComment.created_at | String | The timestamp when the comment was created. |
| Datadog.SecurityComment.user_uuid | String | The UUID of the user who created the comment. |
| Datadog.SecurityComment.text | String | The comment text content. |
| Datadog.SecurityComment.user.name | String | The name of the user who created the comment. |
| Datadog.SecurityComment.user.handle | String | The handle of the user who created the comment. |
Command example
!datadog-add-signal-comment event_id=AZm-wsEuAACEnzdcj-YEigAA comment="Investigating this signal"
!datadog-add-signal-comment comment="False positive confirmed"
datadog-list-signal-comments
List all comments for a security signal in Datadog Cloud SIEM.
Base Command
datadog-list-signal-comments
Input
| Argument Name | Description | Required |
|---|---|---|
| event_id | The event ID of the security signal to list comments for. If not provided, will attempt to get it from the current incident. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Datadog.SecurityComment.id | String | The unique identifier of the comment. |
| Datadog.SecurityComment.created_at | String | The timestamp when the comment was created. |
| Datadog.SecurityComment.user_uuid | String | The UUID of the user who created the comment. |
| Datadog.SecurityComment.text | String | The comment text content. |
| Datadog.SecurityComment.user.name | String | The name of the user who created the comment. |
| Datadog.SecurityComment.user.handle | String | The handle of the user who created the comment. |
Command example
!datadog-list-signal-comments event_id=AZm-wsEuAACEnzdcj-YEigAA
!datadog-list-signal-comments
datadog-get-rule
Get a specific security monitoring rule by ID from Datadog Cloud SIEM.
Base Command
datadog-get-rule
Input
| Argument Name | Description | Required |
|---|---|---|
| rule_id | The unique identifier of the security rule to retrieve. If not provided, will attempt to get it from the current incident. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Datadog.SecurityRule.id | String | The unique identifier of the security rule. |
| Datadog.SecurityRule.name | String | The name of the security rule. |
| Datadog.SecurityRule.type | String | The type of the security rule. |
| Datadog.SecurityRule.isEnabled | Boolean | Whether the security rule is enabled. |
| Datadog.SecurityRule.createdAt | String | Timestamp when the rule was created. |
| Datadog.SecurityRule.message | String | Message for the security rule. |
| Datadog.SecurityRule.queries | Unknown | Queries associated with the security rule. |
| Datadog.SecurityRule.cases | Unknown | Cases (severity and notifications) for the rule. |
| Datadog.SecurityRule.options | Unknown | Options for the security rule. |
| Datadog.SecurityRule.tags | Unknown | Tags associated with the security rule. |
| Datadog.SecurityRule.url | String | URL to view the security rule in Datadog UI. |
| Datadog.SecurityRule.raw | Unknown | The raw rule object returned by the API. |
Command example
!datadog-get-rule rule_id=abc-123-def
!datadog-get-rule
datadog-query-logs
Query logs in Datadog Cloud SIEM with optional filtering for security investigations.
Base Command
datadog-query-logs
Input
| Argument Name | Description | Required |
|---|---|---|
| query | Custom search query string. Uses Datadog search syntax. Required unless running from an incident with a Datadog Security Signal (will use rule’s query as fallback). | Optional |
| from_date | Start date for the search. Format can be relative (e.g., “7 days ago”) or absolute (e.g., “2023-01-01T00:00:00Z”). Default: -7days. | Optional |
| to_date | End date for the search. Format can be relative (e.g., “now”) or absolute (e.g., “2023-01-01T23:59:59Z”). Default: now. | Optional |
| sort | Sort order for results. Possible values are: asc, desc. Default: desc. | Optional |
| limit | Maximum number of results to return. Default: 50. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| Datadog.Log.id | String | The unique identifier of the log entry. |
| Datadog.Log.timestamp | String | The timestamp when the log was generated. |
| Datadog.Log.message | String | The log message content. |
| Datadog.Log.service | String | The service that generated the log. |
| Datadog.Log.host | String | The host that generated the log. |
| Datadog.Log.source | String | The source of the log entry. |
| Datadog.Log.status | String | The status/level of the log entry. |
| Datadog.Log.tags | Unknown | List of tags associated with the log entry. |
| Datadog.Log.url | String | URL to view the log in Datadog UI. |
| Datadog.Log.raw | Unknown | The raw log object returned by the API. |
Command example
!datadog-query-logs query="source:nginx status:error" from_date="-1hour" limit=50
!datadog-query-logs
datadog-update-signal-assignee
Update a security signal’s assignee in Datadog Cloud SIEM.
Base Command
datadog-update-signal-assignee
Input
| Argument Name | Description | Required |
|---|---|---|
| signal_id | The unique identifier of the security signal to update. If not provided, will attempt to get it from the current incident. | Optional |
| assignee | Name or email of the user to assign to the security signal. Leave empty to unassign. | Optional |
Context Output
Same as datadog-get-signal command.
Command example
!datadog-update-signal-assignee signal_id=AZm-wsEuAACEnzdcj-YEigAA assignee=user@example.com
!datadog-update-signal-assignee assignee=""
datadog-update-suppression
Update an existing suppression rule by ID.
Base Command
datadog-update-suppression
Input
| Argument Name | Description | Required |
|---|---|---|
| rule_id | The unique identifier of the suppression rule. | Required |
| enabled | Enable or disable the suppression rule. | Optional |
| name | New name for the suppression rule. | Optional |
| description | New description for the suppression rule. | Optional |
| data_exclusion_query | New data exclusion query for the suppression rule. | Optional |
Context Output
Same as datadog-get-rule command.
Command example
!datadog-update-suppression rule_id=abc-123 enabled=false
datadog-list-suppressions
List all suppressions affecting a specific security monitoring rule.
Base Command
datadog-list-suppressions
Input
| Argument Name | Description | Required |
|---|---|---|
| rule_id | The unique identifier of the security rule. | Required |
Context Output
Returns list of suppression rules associated with the specified rule.
Command example
!datadog-list-suppressions rule_id=abc-123
datadog-list-security-filters
List all security filters from Datadog Cloud SIEM.
Base Command
datadog-list-security-filters
Input
There are no input arguments for this command.
Context Output
Returns list of security filters.
Command example
!datadog-list-security-filters
datadog-list-signal-notification-rules
List all signal notification rules from Datadog Cloud SIEM.
Base Command
datadog-list-signal-notification-rules
Input
There are no input arguments for this command.
Context Output
Returns list of signal notification rules.
Command example
!datadog-list-signal-notification-rules
datadog-list-vulnerability-notification-rules
List all vulnerability notification rules from Datadog Cloud SIEM.
Base Command
datadog-list-vulnerability-notification-rules
Input
There are no input arguments for this command.
Context Output
Returns list of vulnerability notification rules.
Command example
!datadog-list-vulnerability-notification-rules
datadog-bitsai-get-investigation
Get the BitsAI investigation for a security signal.
Base Command
datadog-bitsai-get-investigation
Input
| Argument Name | Description | Required |
|---|---|---|
| signal_id | The unique identifier of the security signal. | Required |
Context Output
Returns BitsAI investigation analysis for the signal.
Command example
!datadog-bitsai-get-investigation signal_id=AZm-wsEuAACEnzdcj-YEigAA
datadog-list-risk-scores
List risk scores from Datadog Cloud SIEM.
Base Command
datadog-list-risk-scores
Input
| Argument Name | Description | Required |
|---|---|---|
| entity | Filter by entity name or identifier. | Optional |
| from_date | Start date for the risk score query. | Optional |
| to_date | End date for the risk score query. | Optional |
| limit | Maximum number of risk scores to return. Default is 100. | Optional |
Context Output
Returns list of risk scores for monitored entities.
Command example
!datadog-list-risk-scores limit=50
Incident Fetching
This integration supports fetching security signals from Datadog Cloud SIEM as Cortex XSOAR incidents.
Configuration
To enable incident fetching:
- In the integration instance configuration, check Fetch incidents.
- Configure the following parameters:
- First fetch time: How far back to fetch signals on first run (e.g., “3 days”, “7 days”)
- Max fetch: Maximum incidents per fetch cycle (recommended: 50, max: 200)
- Fetch severity: Comma-separated list of severities to fetch (leave empty for all)
- Fetch state: Signal state to fetch (default: “open”)
- Fetch query: Additional custom filter query (optional)
- Set the Incident type if you want to classify fetched incidents.
Incident Fields
Each fetched incident includes:
- Name: Security signal title
- Occurred: Signal timestamp
- Severity: Mapped from Datadog severity (Low=1, Medium=2, High=3, Critical=4)
- Raw JSON: Complete signal data for mapping and enrichment
Troubleshooting
Authentication Errors
- Verify API Key and APP Key are correct
- Ensure keys have appropriate permissions in Datadog
- Check Server URL matches your Datadog site (e.g., datadoghq.com, datadoghq.eu)
Fetch Issues
- Check First Fetch Time is not too far in the past (max 90 days recommended)
- Verify Fetch Query syntax using Datadog’s query language
- Review integration logs for detailed error messages
Configuration parameters
site— Server URL (required)api_key— API Key (required)app_key— Application Key (required)insecure— Trust any certificate (not secure)proxy— Use system proxy settingsisFetch— Fetch incidentsincidentType— Incident typeincidentFetchInterval— Incidents Fetch Intervalfirst_fetch— First fetch timemax_fetch— Max fetchfetch_severity— Fetch severityfetch_state— Fetch statefetch_query— Fetch querymirror_direction— Incident Mirroring Directionclose_incident— Close Mirrored XSOAR Incident
Commands (18)
-
datadog-add-signal-commentAdd a comment to a security signal in Datadog Cloud SIEM .
-
datadog-bitsai-get-investigationGet the BitsAI investigation for a security signal from Datadog Cloud SIEM . BitsAI provides AI-powered analysis and investigation steps for security signals.
-
datadog-get-ruleGet a specific security monitoring rule by ID from Datadog Cloud SIEM .
-
datadog-get-signalGet a specific security signal by ID from Datadog Cloud SIEM .
-
datadog-list-risk-scoresList risk scores from Datadog Cloud SIEM . Risk scores provide a quantitative assessment of security risks associated with entities in your environment.
-
datadog-list-security-filtersList all security filters from Datadog Cloud SIEM . Security filters control which logs are indexed and analyzed by the Cloud SIEM platform.
-
datadog-list-signal-commentsList all comments for a security signal in Datadog Cloud SIEM .
-
datadog-list-signal-notification-rulesList all signal notification rules from Datadog Cloud SIEM . Signal notification rules define how and when security signals should trigger notifications to specified channels (e.g., email, Slack, PagerDuty).
-
datadog-list-signalsGet a list of security signals from Datadog Cloud SIEM with optional filtering and pagination.
-
datadog-list-suppressionsList all suppressions affecting a specific security monitoring rule in Datadog Cloud SIEM .
-
datadog-list-vulnerability-notification-rulesList all vulnerability notification rules from Datadog Cloud SIEM. Vulnerability notification rules define how and when vulnerability findings should trigger notifications to specified channels (e.g., email, Slack, PagerDuty).
-
datadog-query-logsQuery logs in Datadog Cloud SIEM with optional filtering for security investigations.
-
datadog-update-signal-assigneeUpdate a security signal's assignee in Datadog Cloud SIEM.
-
datadog-update-signal-stateUpdate a security signal's state in Datadog Cloud SIEM.
-
datadog-update-suppressionUpdate an existing suppression rule by ID. Allows updating enabled state, name, description, and data exclusion query. At least one attribute must be provided to update.
-
get-mapping-fieldsGet the mapping fields for Datadog Cloud SIEM security signals. This command is used for debugging the mirroring integration and returns the schema of available fields.
-
get-modified-remote-dataGet the list of signal IDs that were modified since lastUpdate. This command is used for debugging the mirroring integration.
-
get-remote-dataGet remote data from Datadog Cloud SIEM for mirroring. This command is used for debugging the mirroring integration.
"""Unit tests for DatadogCloudSIEM integration. Pytest Unit Tests: all function names must start with "test_" More details: https://xsoar.pan.dev/docs/integrations/unit-testing """ import datetime from unittest.mock import MagicMock, patch import demistomock as demisto # noqa: F401 import pytest from CommonServerPython import CommandResults, DemistoException from DatadogCloudSIEM import ( Assignee, Comment, Log, SecurityRule, SecuritySignal, Triage, add_security_signal_comment_command, add_utc_offset, as_list, calculate_limit, convert_datetime_to_str, flatten_tag_map, get_security_rule_command, get_security_signal_command, get_security_signal_list_command, list_security_signal_comments_command, logs_query_command, map_severity_to_xsoar, test_module, parse_log, parse_security_comment, parse_security_rule, parse_security_signal, remove_none_values, security_signals_search_query, update_security_signal_assignee_command, update_security_signal_state_command, ) @pytest.fixture def configuration(): """Mock Datadog API configuration.""" config = MagicMock() config.api_key = {"apiKeyAuth": "test_api_key", "appKeyAuth": "test_app_key"} # Add attributes that the Datadog API client expects to avoid initialization issues config.assert_hostname = None config.ssl_ca_cert = None config.cert_file = None config.key_file = None config.verify_ssl = True config.proxy = None config.proxy_headers = None config.safe_chars_for_path_param = "" config.retries = None return config @pytest.fixture def mock_api_client(): """Mock ApiClient for Datadog API calls.""" return MagicMock() @pytest.fixture def security_signal_response(): """Sample security signal API response.""" return { "data": { "id": "AQAAAYvz-1234567890", "event_id": "AQAAAYvz-1234567890", "type": "signal", "attributes": { "event_tracker_id": "AQAAAYvz-1234567890", "event_id": "AQAAAYvz-1234567890", "timestamp": "2024-01-15T10:30:00.000Z", "message": "Suspicious login attempt detected from unusual location", "status": "high", "host": "web-server-01", "service": "auth-service", "tags": [ "security:threat", "env:production", "source:aws", ], "custom": { "title": "Brute Force Login Detection", "workflow": { "rule": { "id": "abc-123-def", "name": "Brute Force Login Detection", "ruleType": "log_detection", "ruleTags": ["attack:credential_access", "technique:T1110"], }, "triage": { "state": "open", "archiveComment": "", "archiveReason": "", "assignee": { "id": 12345, "uuid": "550e8400-e29b-41d4-a716-446655440000", "name": "security_analyst", "handle": "analyst@example.com", }, }, }, }, }, } } @pytest.fixture def security_signals_list_response(): """Sample security signals list API response.""" return { "data": [ { "id": "AQAAAYvz-1234567890", "event_id": "AQAAAYvz-1234567890", "type": "signal", "attributes": { "timestamp": "2024-01-15T10:30:00.000Z", "message": "Suspicious login attempt detected", "status": "high", "tags": ["security:threat", "env:production"], "custom": { "workflow": { "rule": { "id": "abc-123", "name": "Brute Force Detection", "ruleType": "log_detection", "ruleTags": ["attack:credential_access"], }, "triage": { "state": "open", "archiveComment": "", "archiveReason": "", }, } }, }, }, { "id": "AQAAAYvz-0987654321", "event_id": "AQAAAYvz-0987654321", "type": "signal", "attributes": { "timestamp": "2024-01-15T09:15:00.000Z", "message": "Malicious file download detected", "status": "critical", "tags": ["security:threat", "env:production"], "custom": { "workflow": { "rule": { "id": "xyz-456", "name": "Malware Detection", "ruleType": "log_detection", "ruleTags": ["attack:execution"], }, "triage": { "state": "under_review", "archiveComment": "", "archiveReason": "", }, } }, }, }, ], "meta": { "page": { "after": "next_page_cursor", } }, } # Test classes organized by command class TestGetSecuritySignalCommand: """Tests for datadog-signal-get command.""" def test_get_security_signal_command_success(self, configuration, security_signal_response): """Test get_security_signal_command with valid signal ID. Given: A valid signal ID and mocked API response When: The get_security_signal_command is executed Then: The command should return the signal data with correct ID and attributes """ args = {"signal_id": "AQAAAYvz-1234567890"} mock_response = MagicMock() mock_response.to_dict.return_value = security_signal_response with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_security_monitoring_signal.return_value = mock_response result = get_security_signal_command(configuration, args) assert isinstance(result, CommandResults) assert result.outputs["id"] == "AQAAAYvz-1234567890" # type: ignore assert result.outputs["severity"] == "high" # type: ignore mock_api_instance.get_security_monitoring_signal.assert_called_once_with(signal_id="AQAAAYvz-1234567890") def test_get_security_signal_command_not_found(self, configuration): """Test get_security_signal_command when signal is not found. Given: A non-existent signal ID When: The get_security_signal_command is executed Then: The command should return a "not found" message with empty outputs """ args = {"signal_id": "non_existent_id"} mock_response = MagicMock() mock_response.to_dict.return_value = {"data": {}} with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_security_monitoring_signal.return_value = mock_response result = get_security_signal_command(configuration, args) assert isinstance(result, CommandResults) assert "No security signal found" in result.readable_output assert result.outputs == {} # type: ignore def test_get_security_signal_command_missing_signal_id(self, configuration): """Test get_security_signal_command without signal_id in args or incident context. Given: No signal_id provided in args and no incident context When: The get_security_signal_command is executed Then: The command should raise a DemistoException with helpful message """ args = {} with ( patch("DatadogCloudSIEM.demisto.incident", return_value={"CustomFields": {}}), pytest.raises(DemistoException, match="signal_id is required"), ): get_security_signal_command(configuration, args) def test_get_security_signal_command_from_incident_context(self, configuration, security_signal_response): """Test get_security_signal_command retrieves signal_id from incident context. Given: No signal_id in args but valid signal_id in incident custom fields When: The get_security_signal_command is executed Then: The command should use the signal_id from incident and return signal data """ args = {} mock_response = MagicMock() mock_response.to_dict.return_value = security_signal_response with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, patch( "DatadogCloudSIEM.demisto.incident", return_value={"CustomFields": {"datadogcloudsiemv2securitysignalid": "AQAAAYvz-1234567890"}}, ), ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_security_monitoring_signal.return_value = mock_response result = get_security_signal_command(configuration, args) assert isinstance(result, CommandResults) assert result.outputs["id"] == "AQAAAYvz-1234567890" # type: ignore def test_get_security_signal_command_api_error(self, configuration): """Test get_security_signal_command handles API errors gracefully. Given: A signal_id that causes an API error When: The get_security_signal_command is executed Then: The command should raise a DemistoException with error details """ args = {"signal_id": "error_signal"} with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, pytest.raises(DemistoException, match="Failed to get security signal"), ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_security_monitoring_signal.side_effect = Exception("API Error") get_security_signal_command(configuration, args) class TestGetSecuritySignalListCommand: """Tests for datadog-signal-list command.""" def test_get_security_signal_list_command_success(self, configuration, security_signals_list_response): """Test get_security_signal_list_command with default parameters. Given: Default limit parameter and mocked API response with 2 signals When: The get_security_signal_list_command is executed Then: The command should return a list of 2 security signals with correct IDs """ args = {"limit": "50"} mock_response = MagicMock() mock_response.to_dict.return_value = security_signals_list_response with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi"), patch("DatadogCloudSIEM.fetch_security_signals") as mock_fetch, ): # Mock the helper function that parses signals mock_signal1 = MagicMock() mock_signal1.to_dict.return_value = {"id": "signal1"} mock_signal1.to_display_dict.return_value = {"ID": "signal1"} mock_signal2 = MagicMock() mock_signal2.to_dict.return_value = {"id": "signal2"} mock_signal2.to_display_dict.return_value = {"ID": "signal2"} mock_fetch.return_value = [mock_signal1, mock_signal2] result = get_security_signal_list_command(configuration, args) assert isinstance(result, CommandResults) assert isinstance(result.outputs, list) # type: ignore assert len(result.outputs) == 2 # type: ignore def test_get_security_signal_list_command_with_filters(self, configuration, security_signals_list_response): """Test get_security_signal_list_command with severity and state filters. Given: State and severity filter parameters When: The get_security_signal_list_command is executed Then: The command should call fetch_security_signals with correct filter query containing state and severity """ args = { "state": "open", "severity": "high", "limit": "50", } mock_response = MagicMock() mock_response.to_dict.return_value = security_signals_list_response with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, patch("DatadogCloudSIEM.fetch_security_signals") as mock_fetch, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.search_security_monitoring_signals.return_value = mock_response # Mock fetch to return signals mock_signal = MagicMock() mock_signal.to_dict.return_value = {"id": "signal1"} mock_signal.to_display_dict.return_value = {"ID": "signal1"} mock_fetch.return_value = [mock_signal] result = get_security_signal_list_command(configuration, args) assert isinstance(result, CommandResults) # Verify that fetch_security_signals was called with the correct filter query mock_fetch.assert_called_once() call_kwargs = mock_fetch.call_args[1] filter_query = call_kwargs.get("filter_query") # Verify the filter query contains the expected filters assert "@workflow.triage.state:open" in filter_query assert "status:(high OR critical)" in filter_query def test_get_security_signal_list_command_no_results(self, configuration): """Test get_security_signal_list_command when no signals are found. Given: A valid request that returns no signals When: The get_security_signal_list_command is executed Then: The command should return a "no signals found" message with empty outputs """ args = {"limit": "50"} mock_response = MagicMock() mock_response.to_dict.return_value = {"data": [], "meta": {}} with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.search_security_monitoring_signals.return_value = mock_response results = get_security_signal_list_command(configuration, args) assert isinstance(results, CommandResults) assert "No security signals found" in results.readable_output assert results.outputs == [] # type: ignore def test_get_security_signal_list_command_with_custom_query(self, configuration): """Test get_security_signal_list_command with custom query parameter. Given: A custom query string parameter When: The get_security_signal_list_command is executed Then: The command should include the custom query in the filter """ args = {"query": "host:web-server-01", "limit": "10"} with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi"), patch("DatadogCloudSIEM.fetch_security_signals") as mock_fetch, ): mock_signal = MagicMock() mock_signal.to_dict.return_value = {"id": "signal1"} mock_signal.to_display_dict.return_value = {"ID": "signal1"} mock_fetch.return_value = [mock_signal] get_security_signal_list_command(configuration, args) call_kwargs = mock_fetch.call_args[1] filter_query = call_kwargs.get("filter_query") assert "host:web-server-01" in filter_query def test_get_security_signal_list_command_invalid_sort(self, configuration): """Test get_security_signal_list_command with invalid sort parameter. Given: An invalid sort parameter (not 'asc' or 'desc') When: The get_security_signal_list_command is executed Then: The command should raise a DemistoException """ args = {"sort": "invalid", "limit": "50"} with pytest.raises(DemistoException, match="Sort must be either 'asc' or 'desc'"): get_security_signal_list_command(configuration, args) def test_get_security_signal_list_command_with_page_size(self, configuration): """Test get_security_signal_list_command using page_size instead of limit. Given: A page_size parameter instead of limit When: The get_security_signal_list_command is executed Then: The command should use page_size as the limit """ args = {"page_size": "25"} with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi"), patch("DatadogCloudSIEM.fetch_security_signals") as mock_fetch, ): mock_fetch.return_value = [] get_security_signal_list_command(configuration, args) call_kwargs = mock_fetch.call_args[1] assert call_kwargs["limit"] == 25 class TestUpdateSecuritySignalCommand: """Tests for datadog-signal-update command.""" def test_update_security_signal_assignee_command_success(self, configuration): """Test update_security_signal_assignee_command with valid assignee parameter. Given: A valid signal ID and assignee username When: The update_security_signal_assignee_command is executed Then: The command should call the update API with correct payload, then fetch fresh signal data """ args = { "signal_id": "AQAAAYvz-1234567890", "assignee": "security_analyst", } # Mock the get_security_monitoring_signal response (called AFTER update to fetch fresh data) mock_get_response = MagicMock() mock_get_response.to_dict.return_value = { "data": { "id": "AQAAAYvz-1234567890", "event_id": "AQAAAYvz-1234567890", "type": "signal", "attributes": { "event_tracker_id": "AQAAAYvz-1234567890", "event_id": "AQAAAYvz-1234567890", "timestamp": "2024-01-15T10:30:00.000Z", "message": "Test signal", "tags": ["test:tag"], "custom": { "workflow": { "rule": {"id": "rule-123", "name": "Test Rule"}, "triage": { "state": "under_review", "assignee": { "name": "security_analyst", "handle": "analyst@example.com", "uuid": "550e8400-e29b-41d4-a716-446655440000", }, }, } }, }, } } with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, patch("DatadogCloudSIEM.UsersApi") as mock_users_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance # The GET call happens after the update to fetch fresh data mock_api_instance.get_security_monitoring_signal.return_value = mock_get_response # Mock user lookup mock_users_instance = MagicMock() mock_users_api.return_value = mock_users_instance # list_users returns a dict-like object mock_users_instance.list_users.return_value = { "data": [ { "id": "user-123", "attributes": { "name": "security_analyst", "email": "analyst@example.com", }, } ] } result = update_security_signal_assignee_command(configuration, args) assert isinstance(result, CommandResults) assert result.outputs["id"] == "AQAAAYvz-1234567890" # type: ignore # Verify the update API was called with correct assignee payload mock_api_instance.edit_security_monitoring_signal_assignee.assert_called_once() call_args = mock_api_instance.edit_security_monitoring_signal_assignee.call_args assert call_args[1]["signal_id"] == "AQAAAYvz-1234567890" # Verify the body contains the correct assignee UUID from user lookup body = call_args[1]["body"] assert body.data.attributes.assignee.uuid == "user-123" # Verify that after the update, a fresh GET was called to retrieve the updated signal mock_api_instance.get_security_monitoring_signal.assert_called_once_with(signal_id="AQAAAYvz-1234567890") def test_update_security_signal_state_command_success(self, configuration): """Test update_security_signal_state_command with valid state parameter. Given: A valid signal ID, state, archive reason and comment When: The update_security_signal_state_command is executed Then: The command should update the signal state and return the updated signal with archive details """ args = { "signal_id": "AQAAAYvz-1234567890", "state": "archived", "reason": "false_positive", "comment": "This was a false positive alert", } # Mock the get_security_monitoring_signal response mock_get_response = MagicMock() mock_get_response.to_dict.return_value = { "data": { "id": "AQAAAYvz-1234567890", "event_id": "AQAAAYvz-1234567890", "type": "signal", "attributes": { "event_tracker_id": "AQAAAYvz-1234567890", "event_id": "AQAAAYvz-1234567890", "timestamp": "2024-01-15T10:30:00.000Z", "message": "Test signal", "tags": ["test:tag"], "custom": { "workflow": { "rule": {"id": "rule-123", "name": "Test Rule"}, "triage": { "state": "archived", "archiveReason": "false_positive", "archiveComment": "This was a false positive alert", }, } }, }, } } with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_security_monitoring_signal.return_value = mock_get_response result = update_security_signal_state_command(configuration, args) assert isinstance(result, CommandResults) assert result.outputs["id"] == "AQAAAYvz-1234567890" # type: ignore assert result.outputs["triage"]["state"] == "archived" # type: ignore assert result.outputs["triage"]["archive_reason"] == "false_positive" # type: ignore def test_update_security_signal_unassign(self, configuration): """Test update_security_signal_assignee_command with empty assignee to unassign. Given: A signal ID and empty string for assignee parameter When: The update_security_signal_assignee_command is executed Then: The command should unassign the signal (empty UUID) """ args = { "signal_id": "AQAAAYvz-1234567890", "assignee": "", } mock_get_response = MagicMock() mock_get_response.to_dict.return_value = { "data": { "id": "AQAAAYvz-1234567890", "event_id": "AQAAAYvz-1234567890", "attributes": { "event_tracker_id": "AQAAAYvz-1234567890", "event_id": "AQAAAYvz-1234567890", "timestamp": "2024-01-15T10:30:00.000Z", "custom": { "workflow": { "rule": {"id": "rule-123"}, "triage": {"state": "open"}, } }, }, } } with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_security_monitoring_signal.return_value = mock_get_response result = update_security_signal_assignee_command(configuration, args) assert isinstance(result, CommandResults) # Verify assignee update was called with empty UUID call_args = mock_api_instance.edit_security_monitoring_signal_assignee.call_args body = call_args[1]["body"] assert body.data.attributes.assignee.uuid == "" def test_update_security_signal_invalid_state(self, configuration): """Test update_security_signal_state_command with invalid state parameter. Given: A signal ID and invalid state value When: The update_security_signal_state_command is executed Then: The command should raise a DemistoException with valid states """ args = {"signal_id": "AQAAAYvz-1234567890", "state": "invalid_state"} with pytest.raises(DemistoException, match="Invalid state"): update_security_signal_state_command(configuration, args) def test_update_security_signal_multiple_users_found(self, configuration): """Test update_security_signal_assignee_command when user lookup returns multiple results. Given: A signal ID and assignee that matches multiple users When: The update_security_signal_assignee_command is executed Then: The command should raise a DemistoException indicating ambiguity """ args = {"signal_id": "AQAAAYvz-1234567890", "assignee": "john"} with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi"), patch("DatadogCloudSIEM.UsersApi") as mock_users_api, pytest.raises(DemistoException, match="Could not determine the user to assign"), ): mock_users_instance = MagicMock() mock_users_api.return_value = mock_users_instance mock_users_instance.list_users.return_value = { "data": [ { "id": "user-1", "attributes": { "name": "John Doe", "email": "john.doe@example.com", }, }, { "id": "user-2", "attributes": { "name": "John Smith", "email": "john.smith@example.com", }, }, ] } update_security_signal_assignee_command(configuration, args) def test_update_security_signal_user_not_found(self, configuration): """Test update_security_signal_assignee_command when user lookup returns no results. Given: A signal ID and assignee that matches no users When: The update_security_signal_assignee_command is executed Then: The command should raise a DemistoException """ args = {"signal_id": "AQAAAYvz-1234567890", "assignee": "nonexistent"} with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi"), patch("DatadogCloudSIEM.UsersApi") as mock_users_api, pytest.raises(DemistoException, match="Could not determine any user"), ): mock_users_instance = MagicMock() mock_users_api.return_value = mock_users_instance mock_users_instance.list_users.return_value = {"data": []} update_security_signal_assignee_command(configuration, args) class TestGetSecurityRuleCommand: """Tests for datadog-rule-get command.""" def test_get_security_rule_command_success(self, configuration): """Test get_security_rule_command with valid rule ID. Given: A valid rule ID and mocked API response When: The get_security_rule_command is executed Then: The command should return the rule data with correct ID and details """ args = {"rule_id": "rule-abc-123"} mock_response = MagicMock() mock_response.to_dict.return_value = { "id": "rule-abc-123", "name": "Brute Force Login Detection", "type": "log_detection", "isEnabled": True, "createdAt": "2024-01-01T00:00:00+00:00", "message": "Detects brute force login attempts", "queries": [{"query": "source:auth status:error"}], "tags": ["security", "authentication"], } with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_security_monitoring_rule.return_value = mock_response result = get_security_rule_command(configuration, args) assert isinstance(result, CommandResults) assert result.outputs["id"] == "rule-abc-123" # type: ignore assert result.outputs["name"] == "Brute Force Login Detection" # type: ignore assert result.outputs["type"] == "log_detection" # type: ignore mock_api_instance.get_security_monitoring_rule.assert_called_once_with(rule_id="rule-abc-123") def test_get_security_rule_command_from_incident(self, configuration): """Test get_security_rule_command retrieves rule_id from incident context. Given: No rule_id in args but valid rule_id in incident custom fields When: The get_security_rule_command is executed Then: The command should use the rule_id from incident and return rule data """ args = {} mock_response = MagicMock() mock_response.to_dict.return_value = { "id": "rule-from-incident", "name": "Test Rule", "type": "log_detection", "isEnabled": True, } with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, patch( "DatadogCloudSIEM.demisto.incident", return_value={"CustomFields": {"datadogcloudsiemv2securitysignalruleid": "rule-from-incident"}}, ), ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_security_monitoring_rule.return_value = mock_response result = get_security_rule_command(configuration, args) assert isinstance(result, CommandResults) assert result.outputs["id"] == "rule-from-incident" # type: ignore def test_get_security_rule_command_not_found(self, configuration): """Test get_security_rule_command when rule is not found. Given: A non-existent rule ID When: The get_security_rule_command is executed Then: The command should return a "not found" message """ args = {"rule_id": "non-existent-rule"} mock_response = MagicMock() mock_response.to_dict.return_value = {} with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_security_monitoring_rule.return_value = mock_response result = get_security_rule_command(configuration, args) assert isinstance(result, CommandResults) assert "No security rule found" in result.readable_output class TestSecuritySignalCommentCommands: """Tests for datadog-signal-comment-add and datadog-signal-comment-list commands.""" def test_add_security_signal_comment_command_success(self, configuration): """Test add_security_signal_comment_command with valid parameters. Given: A valid event ID and comment text with mocked API response When: The add_security_signal_comment_command is executed Then: The command should add the comment and return the comment data with user information """ args = { "event_id": "AQAAAYvz-1234567890", "comment": "Investigating this security signal", } # Mock requests.post response mock_requests_response = MagicMock() mock_requests_response.ok = True mock_requests_response.json.return_value = { "data": { "id": "comment-123", "attributes": { "comment_id": "comment-123", "created_at": "2024-01-15T10:30:00+00:00", "user_uuid": "user-uuid-123", "text": "Investigating this security signal", }, } } # Mock UsersApi response for user lookup mock_user_response = MagicMock() mock_user_response.to_dict.return_value = { "data": { "id": "user-uuid-123", "attributes": { "name": "John Doe", "handle": "john.doe@example.com", }, } } with ( patch("DatadogCloudSIEM.requests.post", return_value=mock_requests_response), patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.UsersApi") as mock_users_api, ): mock_users_instance = MagicMock() mock_users_api.return_value = mock_users_instance mock_users_instance.get_user.return_value = mock_user_response result = add_security_signal_comment_command(configuration, args) assert isinstance(result, CommandResults) assert result.outputs["id"] == "comment-123" # type: ignore assert result.outputs["text"] == "Investigating this security signal" # type: ignore assert result.outputs["user"]["name"] == "John Doe" # type: ignore def test_add_security_signal_comment_command_missing_comment(self, configuration): """Test add_security_signal_comment_command without comment text. Given: An event ID but no comment text When: The add_security_signal_comment_command is executed Then: The command should raise a DemistoException """ args = {"event_id": "AQAAAYvz-1234567890"} with pytest.raises(DemistoException, match="comment is required"): add_security_signal_comment_command(configuration, args) def test_add_security_signal_comment_command_api_error(self, configuration): """Test add_security_signal_comment_command when API request fails. Given: Valid parameters but API request fails When: The add_security_signal_comment_command is executed Then: The command should raise a DemistoException with API error details """ args = {"event_id": "AQAAAYvz-1234567890", "comment": "Test comment"} mock_requests_response = MagicMock() mock_requests_response.ok = False mock_requests_response.status_code = 403 mock_requests_response.text = "Forbidden" with ( patch("DatadogCloudSIEM.requests.post", return_value=mock_requests_response), pytest.raises(DemistoException, match="API request failed with status 403"), ): add_security_signal_comment_command(configuration, args) def test_list_security_signal_comments_command_success(self, configuration): """Test list_security_signal_comments_command with valid event ID. Given: A valid event ID and mocked API response with 2 comments When: The list_security_signal_comments_command is executed Then: The command should return a list of 2 comments with user information resolved """ args = {"event_id": "AQAAAYvz-1234567890"} # Mock requests.get response mock_requests_response = MagicMock() mock_requests_response.json.return_value = { "data": [ { "id": "comment-123", "attributes": { "comment_id": "comment-123", "created_at": "2024-01-15T10:30:00+00:00", "user_uuid": "user-uuid-123", "text": "First comment", }, }, { "id": "comment-456", "attributes": { "comment_id": "comment-456", "created_at": "2024-01-15T11:00:00+00:00", "user_uuid": "user-uuid-456", "text": "Second comment", }, }, ] } # Mock UsersApi responses for user lookups mock_user1_response = MagicMock() mock_user1_response.to_dict.return_value = { "data": { "id": "user-uuid-123", "attributes": { "name": "John Doe", "handle": "john.doe@example.com", }, } } mock_user2_response = MagicMock() mock_user2_response.to_dict.return_value = { "data": { "id": "user-uuid-456", "attributes": { "name": "Jane Smith", "handle": "jane.smith@example.com", }, } } with ( patch("DatadogCloudSIEM.requests.get", return_value=mock_requests_response), patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.UsersApi") as mock_users_api, ): mock_users_instance = MagicMock() mock_users_api.return_value = mock_users_instance # Return different user data based on which UUID is requested mock_users_instance.get_user.side_effect = [ mock_user1_response, mock_user2_response, ] result = list_security_signal_comments_command(configuration, args) assert isinstance(result, CommandResults) assert isinstance(result.outputs, list) # type: ignore assert len(result.outputs) == 2 # type: ignore assert result.outputs[0]["id"] == "comment-123" # type: ignore assert result.outputs[0]["text"] == "First comment" # type: ignore assert result.outputs[1]["id"] == "comment-456" # type: ignore def test_list_security_signal_comments_command_no_comments(self, configuration): """Test list_security_signal_comments_command when no comments exist. Given: A valid event ID with no comments When: The list_security_signal_comments_command is executed Then: The command should return a "no comments found" message """ args = {"event_id": "AQAAAYvz-1234567890"} mock_requests_response = MagicMock() mock_requests_response.json.return_value = {"data": []} with ( patch("DatadogCloudSIEM.requests.get", return_value=mock_requests_response), patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.UsersApi"), ): result = list_security_signal_comments_command(configuration, args) assert isinstance(result, CommandResults) assert "No comments found" in result.readable_output assert result.outputs == [] # type: ignore class TestLogsQueryCommand: """Tests for datadog-logs-query command.""" @pytest.fixture def logs_query_response(self): """Sample logs search API response.""" return { "data": [ { "id": "log-12345", "type": "log", "attributes": { "timestamp": "2024-01-15T10:30:00.000Z", "message": "User login attempt from 192.168.1.100", "service": "auth-service", "host": "web-server-01", "source": "nginx", "status": "info", "tags": ["env:production", "team:security"], }, }, { "id": "log-12346", "type": "log", "attributes": { "timestamp": "2024-01-15T10:31:00.000Z", "message": "Failed login attempt detected", "service": "auth-service", "host": "web-server-01", "source": "nginx", "status": "warn", "tags": ["env:production", "team:security"], }, }, ], "meta": {"page": {"after": "next_cursor"}}, } def test_logs_query_command_success(self, configuration, logs_query_response): """Test logs_query_command with default parameters. Given: A basic query and limit parameter with mocked API response containing 2 logs When: The logs_query_command is executed Then: The command should return a list of 2 logs with correct IDs and verify query is passed to API """ args = {"query": "*", "limit": "50"} mock_response = MagicMock() mock_response.to_dict.return_value = logs_query_response with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.LogsApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.list_logs.return_value = mock_response result = logs_query_command(configuration, args) assert isinstance(result, CommandResults) assert isinstance(result.outputs, list) # type: ignore assert len(result.outputs) == 2 # type: ignore assert result.outputs[0]["id"] == "log-12345" # type: ignore # Verify that the API was called with the correct query in the body mock_api_instance.list_logs.assert_called_once() call_args = mock_api_instance.list_logs.call_args body = call_args[1]["body"] assert body.filter.query == "*" def test_logs_query_command_with_filters(self, configuration, logs_query_response): """Test logs_query_command with complex query filters. Given: A complex query string with service and status filters When: The logs_query_command is executed Then: The command should call list_logs API with the exact query string in the body """ args = { "query": "service:auth-service status:warn host:web-server-01", "limit": "50", } mock_response = MagicMock() mock_response.to_dict.return_value = logs_query_response with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.LogsApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.list_logs.return_value = mock_response result = logs_query_command(configuration, args) assert isinstance(result, CommandResults) assert isinstance(result.outputs, list) # type: ignore # Verify that the API was called with the correct query in the body mock_api_instance.list_logs.assert_called_once() call_args = mock_api_instance.list_logs.call_args body = call_args[1]["body"] assert body.filter.query == "service:auth-service status:warn host:web-server-01" def test_logs_query_command_no_results(self, configuration): """Test logs_query_command when no logs are found. Given: A valid query that returns no logs When: The logs_query_command is executed Then: The command should return a "no logs found" message """ args = {"query": "*", "limit": "50"} mock_response = MagicMock() mock_response.to_dict.return_value = {"data": [], "meta": {}} with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.LogsApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.list_logs.return_value = mock_response result = logs_query_command(configuration, args) assert isinstance(result, CommandResults) assert "No logs found" in result.readable_output def test_logs_query_command_from_incident_rule(self, configuration, logs_query_response): """Test logs_query_command without query parameter, using rule from incident. Given: No query parameter but valid rule_id in incident context When: The logs_query_command is executed Then: The command should extract query from the rule and use it for log search """ args = {} mock_rule_response = MagicMock() mock_rule_response.to_dict.return_value = { "id": "rule-123", "name": "Test Rule", "type": "log_detection", "isEnabled": True, "queries": [{"query": "source:nginx status:error"}], } mock_logs_response = MagicMock() mock_logs_response.to_dict.return_value = logs_query_response with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_security_api, patch("DatadogCloudSIEM.LogsApi") as mock_logs_api, patch( "DatadogCloudSIEM.demisto.incident", return_value={"CustomFields": {"datadogcloudsiemv2securitysignalruleid": "rule-123"}}, ), ): mock_security_instance = MagicMock() mock_security_api.return_value = mock_security_instance mock_security_instance.get_security_monitoring_rule.return_value = mock_rule_response mock_logs_instance = MagicMock() mock_logs_api.return_value = mock_logs_instance mock_logs_instance.list_logs.return_value = mock_logs_response result = logs_query_command(configuration, args) assert isinstance(result, CommandResults) # Verify rule was fetched and query was extracted mock_security_instance.get_security_monitoring_rule.assert_called_once_with(rule_id="rule-123") # Verify logs were queried with extracted query call_args = mock_logs_instance.list_logs.call_args body = call_args[1]["body"] assert body.filter.query == "source:nginx status:error" def test_logs_query_command_no_query_no_incident(self, configuration): """Test logs_query_command without query and without incident context. Given: No query parameter and no incident context When: The logs_query_command is executed Then: The command should raise a DemistoException """ args = {} with ( patch("DatadogCloudSIEM.demisto.incident", return_value={"CustomFields": {}}), pytest.raises(DemistoException, match="query is required"), ): logs_query_command(configuration, args) class TestFetchIncidents: """Tests for fetch-incidents functionality.""" def test_fetch_incidents_first_fetch(self, configuration, mocker): """Test fetch_incidents on first run (no last_run). Given: First fetch with no previous last_run timestamp When: fetch_incidents is executed Then: Incidents should be created and last_run should be updated with signal timestamp """ params = { "first_fetch": "3 days", "max_fetch": 10, "fetch_severity": "high,critical", "fetch_state": "open", "fetch_query": "", } # Mock demisto functions mock_get_last_run = mocker.patch.object(demisto, "getLastRun", return_value={}) mock_set_last_run = mocker.patch.object(demisto, "setLastRun") mock_incidents = mocker.patch.object(demisto, "incidents") mocker.patch.object(demisto, "debug") # Mock fetch_security_signals helper mock_signal = MagicMock() mock_signal.id = "signal-123" mock_signal.event_id = "signal-123" mock_signal.title = "Test Security Signal" mock_signal.severity = "high" mock_signal.timestamp = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.UTC) mock_signal.message = "Test message content" mock_signal.host = "test-host" mock_signal.tags = ["test:tag"] mock_signal.url = "https://app.datadoghq.com/security/signal?event=signal-123" mock_signal.to_dict.return_value = { "id": "signal-123", "event_id": "signal-123", "title": "Test Security Signal", "severity": "high", "timestamp": "2024-01-15T10:30:00+00:00", "message": "Test message content", "host": "test-host", "tags": ["test:tag"], "url": "https://app.datadoghq.com/security/signal?event=signal-123", } with patch("DatadogCloudSIEM.fetch_security_signals", return_value=[mock_signal]): from DatadogCloudSIEM import fetch_incidents fetch_incidents(configuration, params) # Verify demisto functions were called mock_get_last_run.assert_called_once() mock_set_last_run.assert_called_once() mock_incidents.assert_called_once() # Verify incidents were created incidents_arg = mock_incidents.call_args[0][0] assert len(incidents_arg) == 1 assert incidents_arg[0]["name"] == "Test Security Signal" assert incidents_arg[0]["severity"] == 3 # High severity maps to 3 assert incidents_arg[0]["dbotMirrorId"] == "signal-123" assert incidents_arg[0]["details"] == "Test message content" # Verify rawJSON contains the signal data import json raw_json = json.loads(incidents_arg[0]["rawJSON"]) assert raw_json["id"] == "signal-123" assert raw_json["event_id"] == "signal-123" def test_fetch_incidents_incremental_fetch(self, configuration, mocker): """Test fetch_incidents with existing last_run timestamp. Given: Existing last_run with previous fetch timestamp When: fetch_incidents is executed Then: New incidents should be fetched from the last timestamp and last_run should be updated """ params = { "first_fetch": "3 days", "max_fetch": 10, "fetch_severity": "", "fetch_state": "open", "fetch_query": "", } # Mock demisto functions - simulate incremental fetch with previous timestamp mocker.patch.object( demisto, "getLastRun", return_value={"last_fetch_time": "2024-01-14T10:00:00+00:00"}, ) mock_set_last_run = mocker.patch.object(demisto, "setLastRun") mocker.patch.object(demisto, "incidents") mocker.patch.object(demisto, "debug") # Mock fetch_security_signals helper mock_signal = MagicMock() mock_signal.id = "signal-456" mock_signal.event_id = "signal-456" mock_signal.title = "New Security Signal" mock_signal.severity = "critical" mock_signal.timestamp = datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.UTC) mock_signal.message = "New signal message" mock_signal.host = "test-host" mock_signal.tags = ["test:tag"] mock_signal.url = "https://app.datadoghq.com/security/signal?event=signal-456" mock_signal.to_dict.return_value = { "id": "signal-456", "event_id": "signal-456", "title": "New Security Signal", "severity": "critical", "timestamp": "2024-01-15T10:30:00+00:00", "message": "New signal message", "host": "test-host", "tags": ["test:tag"], "url": "https://app.datadoghq.com/security/signal?event=signal-456", } with patch("DatadogCloudSIEM.fetch_security_signals", return_value=[mock_signal]): from DatadogCloudSIEM import fetch_incidents fetch_incidents(configuration, params) # Verify last_run was updated with new timestamp (as Unix timestamp) mock_set_last_run.assert_called_once() updated_last_run = mock_set_last_run.call_args[0][0] # Expected Unix timestamp for 2024-01-15T10:30:00+00:00 assert updated_last_run["last_fetch_time"] == 1705314600 def test_fetch_incidents_no_results(self, configuration, mocker): """Test fetch_incidents when no new signals are found during incremental fetch. Given: An incremental fetch request with existing last_run that returns no new signals When: fetch_incidents is executed Then: An empty incidents list should be sent to XSOAR and last_run should not change """ params = { "first_fetch": "1 day", "max_fetch": 50, "fetch_severity": "", "fetch_state": "open", "fetch_query": "", } # Mock demisto functions - simulate incremental fetch with previous timestamp mocker.patch.object( demisto, "getLastRun", return_value={"last_fetch_time": "2024-01-14T10:00:00+00:00"}, ) mock_set_last_run = mocker.patch.object(demisto, "setLastRun") mock_incidents = mocker.patch.object(demisto, "incidents") mocker.patch.object(demisto, "debug") with patch("DatadogCloudSIEM.fetch_security_signals", return_value=[]): from DatadogCloudSIEM import fetch_incidents fetch_incidents(configuration, params) # Verify empty incidents list was sent mock_incidents.assert_called_once_with([]) # Verify last_run was not updated (no new incidents) mock_set_last_run.assert_not_called() def test_fetch_incidents_first_fetch_no_results(self, configuration, mocker): """Test fetch_incidents on first run with no results. Given: First fetch with no previous last_run timestamp and no signals returned When: fetch_incidents is executed Then: Empty incidents list should be sent and last_run should be set to from_datetime """ params = { "first_fetch": "1 day", "max_fetch": 50, "fetch_severity": "", "fetch_state": "open", "fetch_query": "", } mocker.patch.object(demisto, "getLastRun", return_value={}) mock_set_last_run = mocker.patch.object(demisto, "setLastRun") mock_incidents = mocker.patch.object(demisto, "incidents") mocker.patch.object(demisto, "debug") with patch("DatadogCloudSIEM.fetch_security_signals", return_value=[]): from DatadogCloudSIEM import fetch_incidents fetch_incidents(configuration, params) # Verify empty incidents list was sent mock_incidents.assert_called_once_with([]) # Verify last_run was set even with no incidents (first run) mock_set_last_run.assert_called_once() # Helper functions and dataclass tests class TestHelperFunctions: """Tests for helper utility functions.""" def test_remove_none_values(self): """Test remove_none_values recursively removes None values. Given: A nested dictionary with None values at various levels When: remove_none_values is called Then: All None values should be removed while preserving non-None values """ data = { "key1": "value1", "key2": None, "key3": { "nested1": "value", "nested2": None, "nested3": {"deep": None, "keep": "value"}, }, "key4": [{"item1": "value", "item2": None}, None, "string"], } result = remove_none_values(data) assert "key1" in result assert "key2" not in result assert "nested2" not in result["key3"] assert "deep" not in result["key3"]["nested3"] assert "keep" in result["key3"]["nested3"] assert len(result["key4"]) == 2 assert "item2" not in result["key4"][0] def test_add_utc_offset(self): """Test add_utc_offset adds timezone.utc timezone to datetime string. Given: An ISO format datetime string without timezone When: add_utc_offset is called Then: The datetime string should have +00:00 timezone.utc offset appended """ dt_str = "2024-01-15T10:30:00" result = add_utc_offset(dt_str) assert "+00:00" in result or "Z" in result assert "2024-01-15" in result assert "10:30:00" in result def test_convert_datetime_to_str(self): """Test convert_datetime_to_str converts datetime objects to ISO strings. Given: A dictionary containing datetime objects When: convert_datetime_to_str is called Then: All datetime objects should be converted to ISO format strings """ dt = datetime.datetime(2024, 1, 15, 10, 30, 0) data = {"timestamp": dt, "nested": {"date": dt}, "string": "keep me"} result = convert_datetime_to_str(data) assert isinstance(result["timestamp"], str) assert "2024-01-15" in result["timestamp"] assert isinstance(result["nested"]["date"], str) assert result["string"] == "keep me" def test_as_list(self): """Test as_list converts various inputs to list format. Given: Various input types (None, single value, list) When: as_list is called Then: Correct list representation should be returned """ assert as_list(None) == [] assert as_list("single") == ["single"] assert as_list([1, 2, 3]) == [1, 2, 3] assert as_list(42) == [42] def test_flatten_tag_map(self): """Test flatten_tag_map converts tag dictionary to key:value strings. Given: A dictionary with various value types (string, list) When: flatten_tag_map is called Then: A flat list of "key:value" strings should be returned """ tag_map = {"env": "prod", "team": ["security", "ops"], "version": 1} result = flatten_tag_map(tag_map) assert "env:prod" in result assert "team:security" in result assert "team:ops" in result assert "version:1" in result assert len(result) == 4 def test_security_signals_search_query(self): """Test security_signals_search_query builds correct query string. Given: Arguments with various filter parameters (state, severity, source, query) When: security_signals_search_query is called Then: A properly formatted Datadog search query string should be returned with all filters """ # Test with multiple filters args = { "state": "open", "severity": "high", "source": "aws", "query": "host:web-server", } query = security_signals_search_query(args) assert "@workflow.triage.state:open" in query assert "status:(high OR critical)" in query assert "source:aws" in query assert "host:web-server" in query assert " AND " in query # Test with no filters (should still have the default rule type filter) query_empty = security_signals_search_query({}) assert '@workflow.rule.type:("Log Detection" OR "Signal Correlation")' in query_empty def test_calculate_limit(self): """Test calculate_limit function. Given: Various combinations of limit and page_size parameters When: calculate_limit is called Then: The correct limit value should be returned with proper precedence rules """ # page_size takes precedence assert calculate_limit(100, 50) == 50 # Use limit when no page_size assert calculate_limit(100, None) == 100 # Use default when both None assert calculate_limit(None, None) == 50 # Test page_size of zero falls through to default (0 is falsy in Python) assert calculate_limit(None, 0) == 50 # Test invalid page_size (negative) with pytest.raises(DemistoException, match="page size should be greater than zero"): calculate_limit(None, -1) def test_map_severity_to_xsoar(self): """Test map_severity_to_xsoar function. Given: Various Datadog severity levels (info, low, medium, high, critical, unknown, None) When: map_severity_to_xsoar is called Then: The correct XSOAR severity number should be returned for each level """ assert map_severity_to_xsoar("info") == 1 assert map_severity_to_xsoar("low") == 1 assert map_severity_to_xsoar("medium") == 2 assert map_severity_to_xsoar("high") == 3 assert map_severity_to_xsoar("critical") == 4 assert map_severity_to_xsoar("unknown") == 0 assert map_severity_to_xsoar(None) == 0 assert map_severity_to_xsoar("") == 0 class TestParsingFunctions: """Tests for parsing functions.""" def test_parse_security_comment(self): """Test parse_security_comment extracts comment data correctly. Given: Raw comment data from Datadog API When: parse_security_comment is called Then: A Comment object with correct attributes should be returned """ data = { "id": "comment-123", "attributes": { "comment_id": "comment-456", "created_at": "2024-01-15T10:30:00+00:00", "user_uuid": "user-uuid-123", "text": "Test comment", }, } comment = parse_security_comment(data) assert isinstance(comment, Comment) assert comment.id == "comment-123" assert comment.created_at == "2024-01-15T10:30:00+00:00" assert comment.user_uuid == "user-uuid-123" assert comment.text == "Test comment" def test_parse_security_rule(self): """Test parse_security_rule extracts rule data correctly. Given: Raw rule data from Datadog API When: parse_security_rule is called Then: A SecurityRule object with correct attributes should be returned """ data = { "id": "rule-123", "name": "Test Rule", "type": "log_detection", "isEnabled": True, "createdAt": "2024-01-01T00:00:00+00:00", "queries": [{"query": "source:nginx"}], "tags": ["security", "auth"], } rule = parse_security_rule(data) assert isinstance(rule, SecurityRule) assert rule.id == "rule-123" assert rule.name == "Test Rule" assert rule.type == "log_detection" assert rule.is_enabled is True assert len(rule.tags) == 2 # type: ignore def test_parse_security_signal(self, security_signal_response): """Test parse_security_signal extracts signal data correctly. Given: Raw signal data from Datadog API with nested workflow and triage When: parse_security_signal is called Then: A SecuritySignal object with correct nested attributes should be returned """ signal = parse_security_signal(security_signal_response["data"]) assert isinstance(signal, SecuritySignal) assert signal.id == "AQAAAYvz-1234567890" assert signal.event_id == "AQAAAYvz-1234567890" assert signal.severity == "high" assert signal.title == "Brute Force Login Detection" assert signal.triage is not None assert signal.triage.state == "open" assert signal.triage.assignee is not None assert signal.triage.assignee.name == "security_analyst" assert signal.rule_id == "abc-123-def" def test_parse_log(self): """Test parse_log extracts log data correctly. Given: Raw log data from Datadog API When: parse_log is called Then: A Log object with correct attributes should be returned """ data = { "id": "log-123", "attributes": { "timestamp": "2024-01-15T10:30:00.000Z", "message": "Test log message", "service": "web-service", "host": "server-01", "source": "nginx", "status": "info", "tags": ["env:prod", "team:ops"], }, } log = parse_log(data) assert isinstance(log, Log) assert log.id == "log-123" assert log.message == "Test log message" assert log.service == "web-service" assert log.host == "server-01" assert len(log.tags) == 2 # type: ignore class TestDataclassMethods: """Tests for dataclass methods (to_dict, to_display_dict, build_url).""" def test_security_signal_build_url(self): """Test SecuritySignal.build_url constructs correct URLs. Given: A SecuritySignal object with a signal ID When: build_url method is called Then: A properly formatted Datadog security signal URL should be returned """ signal = SecuritySignal( id="signal-123", event_id="signal-123", bits_investigator_verdict="", timestamp=datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.UTC), host="", service="", severity="high", title="", message="", rule_id="", triage=Triage(state="open", archive_comment="", archive_reason="", assignee=Assignee("", "")), tags=[], triggering_log_id="", raw={}, ) url = signal.build_url() assert url == "https://app.datadoghq.com/security/signal?event=signal-123" def test_security_signal_to_dict(self): """Test SecuritySignal.to_dict converts signal to dictionary. Given: A SecuritySignal object with nested triage and assignee When: to_dict method is called Then: A properly formatted dictionary with all fields should be returned """ triage = Triage( state="open", archive_comment="", archive_reason="", assignee=Assignee("John", "john@example.com"), ) signal = SecuritySignal( id="signal-123", event_id="event-123", bits_investigator_verdict="", timestamp=datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.UTC), host="test-host", service="test-service", severity="high", title="Test Signal", message="Test message", rule_id="rule-123", triage=triage, tags=["test:tag"], triggering_log_id="log-123", raw={}, ) result = signal.to_dict() assert result["id"] == "signal-123" assert result["severity"] == "high" assert result["triage"]["state"] == "open" assert result["triage"]["assignee"]["name"] == "John" assert result["rule"]["id"] == "rule-123" def test_security_rule_extract_query(self): """Test SecurityRule.extract_query combines multiple queries with OR. Given: A SecurityRule with multiple query objects When: extract_query method is called Then: Queries should be combined with OR operator """ rule = SecurityRule( id="rule-123", name="Test", type="log_detection", is_enabled=True, created_at=datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.UTC), message="Test rule", queries=[{"query": "source:nginx"}, {"query": "source:apache"}], cases=[], options={}, tags=[], raw={}, ) query = rule.extract_query() assert query == "(source:nginx) OR (source:apache)" def test_security_rule_extract_query_single(self): """Test SecurityRule.extract_query with single query returns query directly. Given: A SecurityRule with one query object When: extract_query method is called Then: The single query string should be returned without OR """ rule = SecurityRule( id="rule-123", name="Test", type="log_detection", is_enabled=True, created_at=datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.UTC), message="Test rule", queries=[{"query": "source:nginx"}], cases=[], options={}, tags=[], raw={}, ) query = rule.extract_query() assert query == "source:nginx" def test_security_rule_extract_query_no_queries(self): """Test SecurityRule.extract_query with no queries returns wildcard. Given: A SecurityRule with no queries When: extract_query method is called Then: A wildcard "*" should be returned """ rule = SecurityRule( id="rule-123", name="Test", type="log_detection", is_enabled=True, created_at=datetime.datetime(2024, 1, 1, 0, 0, 0, tzinfo=datetime.UTC), message="Test rule", queries=[], cases=[], options={}, tags=[], raw={}, ) query = rule.extract_query() assert query == "*" def test_log_to_dict(self): """Test Log.to_dict converts log to dictionary. Given: A Log object with timestamp and tags When: to_dict method is called Then: A properly formatted dictionary should be returned """ log = Log( id="log-123", timestamp=datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.UTC), message="Test message", service="web-service", host="test-host", source="nginx", status="info", tags=["env:prod"], raw={}, ) result = log.to_dict() assert result["id"] == "log-123" assert "timestamp" in result assert result["message"] == "Test message" assert result["service"] == "web-service" assert len(result["tags"]) == 1 def test_log_build_url(self): """Test Log.build_url constructs correct log URLs. Given: A Log object with log ID When: build_url method is called Then: A properly formatted Datadog log URL should be returned """ log = Log( id="log-123", timestamp=datetime.datetime(2024, 1, 15, 10, 30, 0, tzinfo=datetime.UTC), message="", service="", host="", source="", status="", tags=[], raw={}, ) url = log.build_url() assert url == "https://app.datadoghq.com/logs?event=log-123" def test_comment_to_dict(self): """Test Comment.to_dict converts comment to dictionary. Given: A Comment object with user information When: to_dict method is called Then: A properly formatted dictionary with user sub-object should be returned """ comment = Comment( id="comment-123", created_at="2024-01-15T10:30:00+00:00", user_uuid="user-123", text="Test comment", user_name="John Doe", user_handle="john@example.com", ) result = comment.to_dict() assert result["id"] == "comment-123" assert result["text"] == "Test comment" assert result["user"]["name"] == "John Doe" assert result["user"]["handle"] == "john@example.com" class TestModule: """Tests for test-module command.""" def test_module_success(self, configuration): """Test module_test with valid authentication. Given: Valid API configuration When: module_test is executed Then: Should return "ok" indicating successful authentication """ with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.AuthenticationApi") as mock_auth_api, ): mock_auth_instance = MagicMock() mock_auth_api.return_value = mock_auth_instance mock_auth_instance.validate.return_value = None result = test_module(configuration) assert result == "ok" mock_auth_instance.validate.assert_called_once() def test_module_authentication_error(self, configuration): """Test test_module with invalid authentication. Given: Invalid API configuration When: test_module is executed Then: Should return authentication error message """ with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.AuthenticationApi") as mock_auth_api, ): mock_auth_instance = MagicMock() mock_auth_api.return_value = mock_auth_instance mock_auth_instance.validate.side_effect = Exception("Invalid API Key") result = test_module(configuration) assert "Authentication Error" in result class TestGetSecuritySignalInvestigationCommand: """Tests for datadog-signal-investigation-get command.""" def test_get_security_signal_investigation_command_success(self, configuration): """Test get_security_signal_investigation_command with valid signal ID. Given: A valid signal ID and mocked API response with investigation data When: The get_security_signal_investigation_command is executed Then: The command should return the investigation data with verdict and summary """ args = {"signal_id": "signal-123"} mock_requests_response = MagicMock() mock_requests_response.ok = True mock_requests_response.json.return_value = { "data": { "id": "inv-123", "type": "investigation", "attributes": { "verdict": "malicious", "steps": [], }, } } with ( patch("DatadogCloudSIEM.requests.get", return_value=mock_requests_response), patch("DatadogCloudSIEM.ApiClient"), ): from DatadogCloudSIEM import get_security_signal_investigation_command result = get_security_signal_investigation_command(configuration, args) assert isinstance(result, CommandResults) assert result.outputs["signal_id"] == "signal-123" # type: ignore assert result.outputs["verdict"] == "malicious" # type: ignore def test_get_security_signal_investigation_command_api_error(self, configuration): """Test get_security_signal_investigation_command when API request fails. Given: A valid signal ID but API request fails When: The get_security_signal_investigation_command is executed Then: The command should raise a DemistoException with error details """ args = {"signal_id": "signal-123"} mock_requests_response = MagicMock() mock_requests_response.ok = False mock_requests_response.status_code = 404 mock_requests_response.text = "Not found" with ( patch("DatadogCloudSIEM.requests.get", return_value=mock_requests_response), pytest.raises(DemistoException, match="API request failed with status 404"), ): from DatadogCloudSIEM import get_security_signal_investigation_command get_security_signal_investigation_command(configuration, args) class TestUpdateRuleSuppressionCommand: """Tests for datadog-suppression-update command.""" def test_update_rule_suppression_command_success(self, configuration): """Test update_rule_suppression_command with valid parameters. Given: A valid suppression ID and update parameters When: The update_rule_suppression_command is executed Then: The command should update the suppression and return the updated data """ args = { "suppression_id": "suppression-123", "enabled": "true", "name": "Updated Suppression", "description": "Updated description", } mock_response = MagicMock() mock_response.to_dict.return_value = { "data": { "id": "suppression-123", "type": "suppressions", "attributes": { "enabled": True, "name": "Updated Suppression", "description": "Updated description", }, } } with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.update_security_monitoring_suppression.return_value = mock_response from DatadogCloudSIEM import update_rule_suppression_command result = update_rule_suppression_command(configuration, args) assert isinstance(result, CommandResults) mock_api_instance.update_security_monitoring_suppression.assert_called_once() def test_update_rule_suppression_command_missing_id(self, configuration): """Test update_rule_suppression_command without suppression ID. Given: No suppression ID provided When: The update_rule_suppression_command is executed Then: The command should raise a DemistoException """ args = {"enabled": "true"} with pytest.raises(DemistoException, match="suppression_id is required"): from DatadogCloudSIEM import update_rule_suppression_command update_rule_suppression_command(configuration, args) class TestSuppressionsListCommand: """Tests for datadog-suppression-list command.""" def test_suppressions_list_command_success(self, configuration): """Test suppressions_list_command returns list of suppressions. Given: Valid API configuration with rule_id When: The suppressions_list_command is executed Then: The command should return a list of suppressions """ args = {"rule_id": "rule-123"} mock_response = MagicMock() mock_response.to_dict.return_value = { "data": [ { "id": "suppression-1", "type": "suppressions", "attributes": { "enabled": True, "name": "Suppression 1", "description": "Test suppression", "data_exclusion_query": "source:test", "suppression_type": "signal", }, }, { "id": "suppression-2", "type": "suppressions", "attributes": { "enabled": False, "name": "Suppression 2", "description": "Another test suppression", "data_exclusion_query": "", "suppression_type": "signal", }, }, ] } with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.list_security_monitoring_suppressions.return_value = mock_response from DatadogCloudSIEM import suppressions_list_command result = suppressions_list_command(configuration, args) assert isinstance(result, CommandResults) assert isinstance(result.outputs, list) # type: ignore class TestListSecurityFilterCommand: """Tests for datadog-filter-list command.""" def test_list_security_filter_command_success(self, configuration): """Test list_security_filter_command returns list of filters. Given: Valid API configuration When: The list_security_filter_command is executed Then: The command should return a list of security filters """ args = {} mock_response = MagicMock() mock_response.to_dict.return_value = { "data": [ { "id": "filter-1", "type": "security_filters", "attributes": { "name": "Filter 1", "query": "source:aws", "is_enabled": True, }, } ] } with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.list_security_filters.return_value = mock_response from DatadogCloudSIEM import list_security_filter_command result = list_security_filter_command(configuration, args) assert isinstance(result, CommandResults) class TestListSignalNotificationRuleCommand: """Tests for datadog-signal-notification-rule-list command.""" def test_list_signal_notification_rule_command_success(self, configuration): """Test list_signal_notification_rule_command returns list of notification rules. Given: Valid API configuration When: The list_signal_notification_rule_command is executed Then: The command should return a list of signal notification rules """ args = {} mock_response = MagicMock() mock_response.get.return_value = [ { "id": "rule-1", "type": "notification_rules", "attributes": { "name": "Notification Rule 1", "query": "status:high", }, } ] with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_signal_notification_rules.return_value = mock_response from DatadogCloudSIEM import list_signal_notification_rule_command result = list_signal_notification_rule_command(configuration, args) assert isinstance(result, CommandResults) class TestListVulnerabilityNotificationRuleCommand: """Tests for datadog-vulnerability-notification-rule-list command.""" def test_list_vulnerability_notification_rule_command_success(self, configuration): """Test list_vulnerability_notification_rule_command returns list of vulnerability notification rules. Given: Valid API configuration When: The list_vulnerability_notification_rule_command is executed Then: The command should return a list of vulnerability notification rules """ args = {} mock_response = MagicMock() mock_response.get.return_value = [ { "id": "vuln-rule-1", "type": "vulnerability_notification_rules", "attributes": { "name": "Vulnerability Rule 1", "query": "severity:critical", }, } ] with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_vulnerability_notification_rules.return_value = mock_response from DatadogCloudSIEM import list_vulnerability_notification_rule_command result = list_vulnerability_notification_rule_command(configuration, args) assert isinstance(result, CommandResults) class TestListRiskScoresCommand: """Tests for datadog-risk-scores-list command.""" def test_list_risk_scores_command_success(self, configuration): """Test list_risk_scores_command returns list of risk scores. Given: Valid API configuration When: The list_risk_scores_command is executed Then: The command should return a list of risk scores """ args = {} mock_response = MagicMock() mock_response.to_dict.return_value = { "data": [ { "id": "risk-1", "type": "risk_scores", "attributes": { "entity_id": "entity-123", "score": 85, "risk_level": "high", }, } ] } with ( patch("DatadogCloudSIEM.requests.get") as mock_get, patch("DatadogCloudSIEM.ApiClient"), ): mock_get.return_value.json.return_value = mock_response.to_dict() from DatadogCloudSIEM import list_risk_scores_command result = list_risk_scores_command(configuration, args) assert isinstance(result, CommandResults) class TestMirroringCommands: """Tests for mirroring commands (get-remote-data, get-modified-remote-data, get-mapping-fields).""" def test_get_remote_data_command_success(self, configuration, security_signal_response): """Test get_remote_data_command retrieves signal data for mirroring. Given: A valid signal ID in args and params When: The get_remote_data_command is executed Then: The command should return GetRemoteDataResponse with signal data """ args = {"id": "AQAAAYvz-1234567890", "lastUpdate": "0"} params = {"close_incident": False} mock_response = MagicMock() mock_response.to_dict.return_value = security_signal_response with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.get_security_monitoring_signal.return_value = mock_response from DatadogCloudSIEM import get_remote_data_command result = get_remote_data_command(configuration, args, params) from CommonServerPython import GetRemoteDataResponse assert isinstance(result, GetRemoteDataResponse) def test_get_modified_remote_data_command_success(self, configuration, mocker): """Test get_modified_remote_data_command retrieves modified signal IDs. Given: A lastUpdate timestamp in args When: The get_modified_remote_data_command is executed Then: The command should return GetModifiedRemoteDataResponse with modified signal IDs """ args = {"lastUpdate": "2024-01-14T00:00:00Z"} # Mock demisto.debug to prevent output mocker.patch.object(demisto, "debug") mocker.patch.object(demisto, "error") # Create a proper signal response with all required fields mock_response = MagicMock() mock_response.to_dict.return_value = { "data": [ { "id": "signal-1", "event_id": "signal-1", "attributes": { "event_tracker_id": "signal-1", "event_id": "signal-1", "timestamp": "2024-01-15T10:00:00Z", "message": "Test signal", "status": "high", "tags": ["test:tag"], "custom": { "title": "Test Signal", "workflow": { "rule": { "id": "rule-1", "name": "Test Rule", }, "triage": { "state": "open", "archiveComment": "", "archiveReason": "", }, }, }, }, } ], "meta": {}, } with ( patch("DatadogCloudSIEM.ApiClient"), patch("DatadogCloudSIEM.SecurityMonitoringApi") as mock_api, ): mock_api_instance = MagicMock() mock_api.return_value = mock_api_instance mock_api_instance.search_security_monitoring_signals.return_value = mock_response from DatadogCloudSIEM import get_modified_remote_data_command result = get_modified_remote_data_command(configuration, args) from CommonServerPython import GetModifiedRemoteDataResponse assert isinstance(result, GetModifiedRemoteDataResponse) assert len(result.modified_incident_ids) == 1 assert result.modified_incident_ids[0] == "signal-1" def test_get_mapping_fields_command_success(self): """Test get_mapping_fields_command returns field mappings. Given: No arguments needed When: The get_mapping_fields_command is executed Then: The command should return GetMappingFieldsResponse with incident field mappings """ from DatadogCloudSIEM import get_mapping_fields_command from CommonServerPython import GetMappingFieldsResponse result = get_mapping_fields_command() assert isinstance(result, GetMappingFieldsResponse)