SOCRadarIncidentsV4
SOCRadar Incidents v4 API integration with advanced incident management, status reasons, and compliance tracking. Fetches security incidents from SOCRadar platform with proper deduplication and date handling. Supports filtering by alarm type IDs for granular control.
Analytics & SIEM · SOCRadar
Details
| ID | SOCRadarIncidentsV4 |
|---|---|
| Provider | SOCRadar |
| Category | Analytics & SIEM |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10116658 |
| Supported Modules | Agentix XSIAM |
README
SOCRadar Incidents v4.0
Fetch and manage security incidents from SOCRadar’s Incident API v4 with advanced filtering, multi-status selection, and comprehensive incident enrichment.
Overview
SOCRadar is a digital risk protection platform that provides extended threat intelligence and brand protection capabilities. This integration enables XSOAR to ingest security incidents from SOCRadar’s Incident API v4, including:
- Brand Protection: Impersonating domains, phishing attacks, brand abuse
- Cyber Threat Intelligence: Stolen credentials, data leaks, malware infections
- Attack Surface Management: External exposure findings, misconfigurations
- Dark Web Intelligence: Compromised credentials, leaked data from dark web sources
- Supply Chain Security: Third-party risks and vendor security issues
What’s New in v4.0
Major Enhancements
- Multi-Status Filtering: Select multiple statuses (OPEN, CLOSED, ON_HOLD) simultaneously
- Epoch Time Precision: Second-level accuracy for incident fetching - zero duplicates
- Reverse Pagination: Fetches newest incidents first for better performance
- Dynamic Content Extraction: Automatically extracts alarm-specific fields regardless of type
- Parametric Company ID Display: Control whether company ID appears in incidents
- Enhanced Deduplication: Two-layer protection prevents duplicate incidents
- New Commands: Ask analyst, change severity, and more
Technical Improvements
- Interval-based fetching with overlap protection
- Configurable content and entity inclusion
- Integer company ID with validation
- Comprehensive debug logging
- Better error handling and recovery
Key Features
Incident Fetching
- Fetch incidents with configurable interval (default: 1 minute)
- Support for date ranges and epoch timestamps
- Automatic deduplication (tracks last 1000 alarm IDs)
- Reverse pagination for optimal performance
Flexible Filtering
- Multi-Status Selection: OPEN, CLOSED, ON_HOLD (multi-select)
- Severity Levels: LOW, MEDIUM, HIGH, CRITICAL
- Alarm Types: Filter by main type, sub-type, or type ID
- Custom Filters: Tags, assignees, date ranges
Rich Incident Data
- Dynamic CustomFields based on alarm type
- Configurable content extraction (varies by alarm type)
- Optional entity details inclusion
- Optional company ID visibility
Incident Management
- Change alarm status (11 status options)
- Add comments to alarms
- Change assignees
- Add/remove tags
- Request analyst assistance
- Modify severity levels
- Mark as false positive/resolved
Prerequisites
Required
- SOCRadar account with Incident API access
- API Key from SOCRadar platform
- Company ID
- XSOAR 6.x or later
API Access
To obtain your API credentials:
- Log in to SOCRadar Platform
- Navigate to Settings → API & Integrations
- Go to API Options page
- Copy your Company API Key (for Incident API)
- Note your Company ID
Configuration
Integration Settings
| Parameter | Required | Default | Description |
|---|---|---|---|
| Server URL | Yes | https://platform.socradar.com/api |
SOCRadar API base URL |
| API Key | Yes | - | Your Company API Key from SOCRadar |
| Company ID | Yes | - | Your Company ID (integer) |
| Fetch incidents | No | False | Enable automatic incident fetching |
| Incident type | No | - | XSOAR incident type to create |
| Max incidents per fetch | No | 10000 | Maximum incidents per fetch cycle |
| First fetch time | No | 3 days | Initial time range for first fetch |
| Fetch Interval (Minutes) | No | 1 | Time window for subsequent fetches |
Filtering Options
| Parameter | Type | Description |
|---|---|---|
| Status Filter | Multi-select | Select one or more: OPEN, CLOSED, ON_HOLD |
| Severity | Multi-select | Filter by: LOW, MEDIUM, HIGH, CRITICAL |
| Alarm Type IDs | Text | Comma-separated list of type IDs to include |
| Excluded Alarm Type IDs | Text | Comma-separated list of type IDs to exclude |
| Main Alarm Types | Text | Comma-separated main types (e.g., “Brand Protection”) |
| Alarm Sub Types | Text | Comma-separated sub types |
Content Options
| Parameter | Default | Description |
|---|---|---|
| Include Alarm Content | True | Extract content fields to CustomFields |
| Include Related Entities | True | Include entity details in CustomFields |
| Include Company ID | False | Show company ID in incidents (for multi-tenant) |
Installation
From XSOAR Marketplace
- Navigate to Settings → Marketplace
- Search for “SOCRadar”
- Click Install
- Configure integration instance
From GitHub (Manual)
# Clone the content repository
git clone https://github.com/demisto/content.git
cd content/Packs/SOCRadar
# Copy to your XSOAR instance
# Upload via Settings → Integrations → Upload
Setup Guide
Step 1: Create Integration Instance
- Navigate to Settings → Integrations → Servers & Services
- Search for “SOCRadar Incidents v4”
- Click Add instance
Step 2: Configure Basic Settings
Name: SOCRadar Production
Server URL: https://platform.socradar.com/api
API Key: [Your API Key from SOCRadar]
Company ID: [Your Company ID]
Step 3: Configure Fetching (Optional)
✓ Fetches incidents
Incident type: SOCRadar Incident
Max incidents per fetch: 10000
First fetch time: 3 days
Fetch Interval: 1 (minutes)
Step 4: Configure Filters (Optional)
Status Filter:
☑ OPEN
☐ CLOSED
☐ ON_HOLD
Content Options:
☑ Include Alarm Content in Custom Fields
☑ Include Related Entities Details
☐ Include Company ID in Incidents
Step 5: Test Connection
Click Test button to verify:
- API connectivity
- Authentication
- Company ID validity
Commands
Incident Management
socradar-change-alarm-status
Change the status of one or more alarms.
Arguments:
alarm_ids(Required): Comma-separated alarm IDs (e.g., “12345,67890”)status_reason(Required): New statusOPEN,INVESTIGATING,RESOLVED,PENDING_INFOLEGAL_REVIEW,VENDOR_ASSESSMENT,FALSE_POSITIVEDUPLICATE,PROCESSED_INTERNALLY,MITIGATED,NOT_APPLICABLE
comments(Optional): Status change commentscompany_id(Optional): Override default company ID
Example:
!socradar-change-alarm-status alarm_ids="81171696" status_reason="INVESTIGATING" comments="Under review"
From Incident Context:
!socradar-change-alarm-status alarm_ids=${incident.dbotMirrorId} status_reason="RESOLVED"
socradar-mark-false-positive
Mark alarm as false positive (shortcut command).
Arguments:
alarm_id(Required): Alarm IDcomments(Optional): Reason for false positivecompany_id(Optional): Override default company ID
Example:
!socradar-mark-false-positive alarm_id="81171696" comments="Not a real threat"
socradar-mark-resolved
Mark alarm as resolved (shortcut command).
Arguments:
alarm_id(Required): Alarm IDcomments(Optional): Resolution notescompany_id(Optional): Override default company ID
Example:
!socradar-mark-resolved alarm_id="81171696" comments="Issue fixed"
For Notes
socradar-add-comment
Add a comment to an alarm.
Arguments:
alarm_id(Required): Alarm IDuser_email(Required): Email of user posting commentcomment(Required): Comment textcompany_id(Optional): Override default company ID
Example:
!socradar-add-comment alarm_id="81171696" user_email="useremail" comment="Investigating with security team"
socradar-ask-analyst
Request assistance from SOCRadar analyst.
Arguments:
alarm_id(Required): Alarm IDcomment(Required): Message for analystcompany_id(Optional): Override default company ID
Example:
!socradar-ask-analyst alarm_id="81171696" comment="Need help analyzing this credential leak"
Assignment & Organization
socradar-add-assignee
Change alarm assignee(s).(User must be defined the same company)
Arguments:
alarm_id(Required): Alarm IDuser_emails(Required): Comma-separated email addressescompany_id(Optional): Override default company ID
Example:
!socradar-add-assignee alarm_id="81171696" user_emails="useremail"
socradar-add-tag
Add or remove a tag from alarm.
Arguments:
alarm_id(Required): Alarm IDtag(Required): Tag namecompany_id(Optional): Override default company ID
Example:
!socradar-add-tag alarm_id="81171696" tag="reviewed"
socradar-change-severity
Modify alarm severity level.
Arguments:
alarm_id(Required): Alarm IDseverity(Required): New severity (LOW, MEDIUM, HIGH, CRITICAL)company_id(Optional): Override default company ID
Example:
!socradar-change-severity alarm_id="81171696" severity="HIGH"
Testing
socradar-test-fetch
Test the fetch incidents functionality without creating incidents.
Arguments:
first_fetch(Optional): Time range to test (default: “3 days”)limit(Optional): Number of incidents to fetch (default: 5)
Example:
!socradar-test-fetch first_fetch="1 hour" limit="10"
Output:
Found 150 incident(s) from 2024-12-20!
Total available: 1000 records across 10 pages
Using REVERSE PAGINATION (page 10 → page 1)
Sample incidents:
- [81171696] HIGH | OPEN | example.com
Company: 789 | Type: Brand Protection / Impersonating Domain
Custom Fields
The integration creates these CustomFields in XSOAR incidents:
Standard Fields (Always Present)
socradaralarmid: Alarm IDsocradarstatus: Current statussocradarasset: Affected assetsocradaralarmtype: Main alarm typesocradaralarmsubtype: Alarm sub-typesocradaralarmtypeid: Type IDsocradartags: Comma-separated tagssocradarrisklevel: Risk levelsocradaralarmtext: Alarm description (truncated to 1000 chars)
Optional Fields
socradarcompanyid: Company ID (if “Include Company ID” enabled)socradarentities: Related entities (if “Include Entities” enabled)
Dynamic Content Fields (if “Include Content” enabled)
Content structure varies by alarm type. Examples:
Impersonating Domain:
socradarcontentdns_informationsocradarcontentwhois_informationsocradarcontentdomain_status
Stolen Credentials:
socradarcontentcredential_detailssocradarcontentlog_content_linksocradarcontentmalware_family
Data Leak:
socradarcontentsource_full_contentsocradarcontentcompromised_emailssocradarcontentcompromised_domains
Use Cases
1. Brand Protection
Scenario: Detect and respond to phishing domains impersonating your brand.
Configuration:
Main Alarm Types: Brand Protection
Status Filter: OPEN
Severity: HIGH, CRITICAL
Automation:
# Get incident
incident = demisto.incident()
alarm_id = incident.get('dbotMirrorId')
# Check domain details
domain_info = incident.get('CustomFields', {}).get('socradarcontentdns_information')
# Take action
demisto.executeCommand('socradar-add-assignee', {
'alarm_id': alarm_id,
'user_emails': 'useremail@usermail.com'
})
2. Multi-Tenant Operations (MSSP)
Scenario: Manage incidents for multiple customers.
Configuration:
Instance 1:
- Company ID: 789
- Include Company ID: ✓ YES
Instance 2:
- Company ID: 456
- Include Company ID: ✓ YES
Automation:
# Company ID is visible in CustomFields
company_id = incident.get('CustomFields', {}).get('socradarcompanyid')
# Route to appropriate team
if company_id == "789":
assign_to = "useremail"
else:
assign_to = "useremail"
demisto.executeCommand('socradar-add-assignee', {
'alarm_id': incident.get('dbotMirrorId'),
'user_emails': assign_to
})
Troubleshooting
No Incidents Fetched
Check:
- Test connection: Click “Test” button
- Date range: Increase “First fetch time” to “7 days”
- Filters: Remove status/severity filters temporarily
- Debug logs: Check XSOAR server logs
Debug Command:
!socradar-test-fetch first_fetch="7 days" limit="10"
Look for:
[SOCRadar V4.0] Total available: 0 records
If 0 records, no alarms match your filters.
Duplicate Incidents
This should NOT happen in v4.0! If you see duplicates:
- Check fetch interval: Should be ≥ 1 minute
-
Check logs for deduplication stats:
[SOCRadar V4.0] Page 1: Created 5 NEW incidents, skipped 95 duplicates -
Verify epoch time usage:
[SOCRadar V4.0] Time window (epoch seconds): 1734465600 to 1734465660
If duplicates persist, contact support with debug logs.
API Errors
401 Unauthorized:
- Verify API Key is correct
404 Not Found:
- Verify Company ID is correct (integer, not string)
- Check endpoint URLs in debug logs
Rate Limiting:
- Reduce fetch frequency
- Decrease “Max incidents per fetch”
Missing CustomFields
If dynamic content fields not appearing:
- Verify “Include Alarm Content” is enabled
- Create CustomFields manually in XSOAR:
- Settings → Advanced → Fields
- Add fields with type “Short Text”
- Prefix:
socradarcontent
If entities not appearing:
- Verify “Include Related Entities Details” is enabled
Performance Optimization
High-Volume Environments
Recommended Settings:
Max incidents per fetch: 10000
Fetch Interval: 5 (minutes)
Include Alarm Content: ✓ (enabled)
Include Related Entities: ✓ (enabled)
With Filters:
Status Filter: OPEN only
Severity: HIGH, CRITICAL only
This reduces data volume while capturing critical alerts.
Low-Volume Environments
Recommended Settings:
Max incidents per fetch: 1000
Fetch Interval: 1 (minutes)
Include Alarm Content: ✓ (enabled)
Include Related Entities: ✓ (enabled)
Faster response to new incidents.
Support
Official Support
- SOCRadar XSOAR Support: XSOAR@socradar.io
- SOCRadar Support: support@socradar.io
- Platform: platform.socradar.com
Feature Requests
Submit feature requests through:
- SOCRadar platform Request
- XSOAR content repository issues
Version History
v4.0.0 (December 2024)
- Initial release of SOCRadar Incidents v4.0
- Multi-status filtering support
- Epoch time precision for zero duplicates
- Reverse pagination implementation
- Dynamic content extraction
- Enhanced deduplication (2-layer)
- New commands: ask-analyst, change-severity
- Parametric company ID display
- Comprehensive debug logging
License
This integration is provided under the MIT License.
About SOCRadar
SOCRadar is a leading Extended Threat Intelligence (XTI) platform that helps organizations:
- Monitor and protect their digital assets
- Detect brand abuse and phishing attacks
- Identify stolen credentials and data leaks
- Track dark web activities
- Manage attack surface exposure
- Ensure supply chain security
Configuration parameters
apikey— (required)company_id— Company ID (required)isFetch— Fetch incidentsfirst_fetch— First Fetch Timefetch_interval_minutes— Fetch Interval (Minutes)max_fetch— Max incidents per fetchshow_content— Show Alarm Contentstatus— Status Filterseverities— Severity Levelsalarm_type_ids— Alarm Type IDs (Include)excluded_alarm_type_ids— Alarm Type IDs (Exclude)alarm_main_types— Alarm Main Typesexcluded_alarm_main_types— Alarm Main Types (Exclude)alarm_sub_types— Alarm Sub Typesexcluded_alarm_sub_types— Alarm Sub Types (Exclude)include_company_id— Include Company IDinclude_incident_link— Include Incident Linkinclude_mitigation— Include Mitigationinclude_response— Include Responseinclude_detection_and_analysis— Include Detection And Analysisinclude_post_incident_analysis— Include Post Incident Analysisinclude_related_assets— Include Related Assetsinclude_related_entities— Include Related Entitiesinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsincidentType— Incident typeincidentFetchInterval— Incidents Fetch Interval
Commands (8)
-
socradar-add-assigneeadd the assignee(s) of an alarm.
-
socradar-add-commentAdd a comment to an alarm.
-
socradar-add-tagAdd or remove a tag from an alarm.
-
socradar-change-alarm-statusChange the status of one or more alarms.
-
socradar-change-assigneeDeprecatedadd the assignee(s) of an alarm.
-
socradar-mark-false-positiveMark an alarm as false positive.
-
socradar-mark-resolvedMark an alarm as resolved.
-
socradar-test-fetchTest incident fetching to verify alarms are available and date parsing works correctly.
import demistomock as demisto from CommonServerPython import * from CommonServerUserPython import * import urllib3 import traceback from typing import Any from datetime import datetime, timedelta urllib3.disable_warnings() SOCRADAR_API_ENDPOINT = "https://platform.socradar.com/api" SOCRADAR_SEVERITIES = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"] MAX_INCIDENTS_TO_FETCH = 100000 MAX_INCIDENTS_PER_PAGE = 100 STATUS_REASON_MAP = { "OPEN": 0, "INVESTIGATING": 1, "RESOLVED": 2, "PENDING_INFO": 4, "LEGAL_REVIEW": 5, "VENDOR_ASSESSMENT": 6, "FALSE_POSITIVE": 9, "DUPLICATE": 10, "PROCESSED_INTERNALLY": 11, "MITIGATED": 12, "NOT_APPLICABLE": 13, } MESSAGES = { "BAD_REQUEST_ERROR": "An error occurred while fetching the data.", "AUTHORIZATION_ERROR": "Authorization Error: make sure API Key is correctly set.", "RATE_LIMIT_EXCEED_ERROR": "Rate limit exceeded.", } def convert_to_demisto_severity(severity: str) -> int | float: """Convert SOCRadar severity to Demisto severity level""" return { "LOW": IncidentSeverity.LOW, "MEDIUM": IncidentSeverity.MEDIUM, "HIGH": IncidentSeverity.HIGH, "CRITICAL": IncidentSeverity.CRITICAL, "INFO": IncidentSeverity.INFO, }.get(severity.upper(), IncidentSeverity.UNKNOWN) class Client(BaseClient): def __init__(self, base_url: str, api_key: str, company_id: str, verify: bool, proxy: bool): super().__init__(base_url, verify=verify, proxy=proxy) self.api_key = api_key self.company_id = company_id def _get_headers(self) -> dict[str, str]: """Get API request headers""" return {"API-Key": self.api_key} def search_incidents( self, status: list[str] | None = None, severities: list[str] | None = None, alarm_main_types: list[str] | None = None, alarm_sub_types: list[str] | None = None, alarm_type_ids: list[int] | None = None, excluded_alarm_type_ids: list[int] | None = None, excluded_alarm_main_types: list[str] | None = None, excluded_alarm_sub_types: list[str] | None = None, start_date: str | None = None, end_date: str | None = None, limit: int = 20, page: int = 1, # FIX: Added include_total_records parameter. When True, the API returns # total_pages and total_records in the response. Previously this was # hardcoded to "true" and test_fetch_command passed it as a kwarg which # caused a TypeError since search_incidents did not accept it. include_total_records: bool = True, include_company_id: bool = False, ) -> dict[str, Any]: """ Search incidents from SOCRadar API Args: status: List of statuses to filter (OPEN, CLOSED, ON_HOLD) severities: List of severity levels to filter alarm_main_types: List of main alarm types to filter alarm_sub_types: List of alarm subtypes to filter alarm_type_ids: List of alarm type IDs to include excluded_alarm_type_ids: List of alarm type IDs to exclude excluded_alarm_sub_types: List of alarm sub types to exclude excluded_alarm_main_types: List of alarm main types to exclude start_date: Start date for filtering (YYYY-MM-DD) end_date: End date for filtering (YYYY-MM-DD) limit: Number of results per page (max 100) page: Page number for pagination include_total_records: Whether to request total record counts from the API include_company_id: Whether to request company_id in alarm data from the API API Response Structure: { "data": { "alarms": [...], "total_pages": 3077, "total_records": 6153 }, "is_success": true, "message": "Success" } """ # FIX: Use the include_total_records parameter instead of hardcoding "true". # This allows callers like test_fetch_command to disable counting for performance. params: dict[str, Any] = { "limit": min(limit, 100), "page": page, "include_total_records": "true" if include_total_records else "false", } if status: params["status"] = status if severities: params["severities"] = severities if alarm_main_types: params["alarm_main_types"] = alarm_main_types if alarm_sub_types: params["alarm_sub_types"] = alarm_sub_types if alarm_type_ids: params["alarm_type_ids"] = alarm_type_ids if excluded_alarm_type_ids: params["excluded_alarm_type_ids"] = excluded_alarm_type_ids if start_date: params["start_date"] = start_date if end_date: params["end_date"] = end_date if excluded_alarm_main_types: params["excluded_alarm_main_types"] = excluded_alarm_main_types if excluded_alarm_sub_types: params["excluded_alarm_sub_types"] = excluded_alarm_sub_types if include_company_id: params["include_company_id"] = "true" url_suffix = f"/company/{self.company_id}/incidents/v4" demisto.debug(f"[SOCRadar] Requesting incidents from: {url_suffix}") demisto.debug(f"[SOCRadar] Request params: {params}") try: response = self._http_request( method="GET", url_suffix=url_suffix, params=params, headers=self._get_headers(), timeout=60, resp_type="json", ) demisto.debug(f"[SOCRadar] Response type: {type(response)}") if isinstance(response, dict): demisto.debug(f"[SOCRadar] Response keys: {list(response.keys())}") if not response.get("is_success", True): error_msg = response.get("message", "Unknown error") demisto.error(f"[SOCRadar] API Error: {error_msg}") raise DemistoException(f"API Error: {error_msg}") data_obj = response.get("data", {}) # When include_total_records is false, API returns data as a list # instead of a dict with "alarms", "total_pages", "total_records" keys. if isinstance(data_obj, list): alarms = data_obj total_pages = 1 total_records = len(alarms) else: alarms = data_obj.get("alarms", []) total_pages = data_obj.get("total_pages", 1) total_records = data_obj.get("total_records", len(alarms)) demisto.debug(f"[SOCRadar] Received {len(alarms)} alarms from page {page}") demisto.debug(f"[SOCRadar] Total records: {total_records}, Total pages: {total_pages}") if alarms and len(alarms) > 0: sample = alarms[0] demisto.debug(f"[SOCRadar] Sample alarm keys: {list(sample.keys())}") demisto.debug(f"[SOCRadar] Sample alarm_id: {sample.get('alarm_id')}") demisto.debug(f"[SOCRadar] Sample status: {sample.get('status')}") return { "is_success": response.get("is_success"), "message": response.get("message"), "response_code": response.get("response_code"), "data": alarms, "total_pages": int(total_pages), "total_records": int(total_records), "current_page": page, } else: demisto.error(f"[SOCRadar] Unexpected response type: {type(response)}") raise DemistoException("Unexpected response format from API") except Exception as e: demisto.error(f"[SOCRadar] HTTP Request failed: {str(e)}") demisto.error(f"[SOCRadar] Traceback: {traceback.format_exc()}") raise def change_alarm_status( self, alarm_ids: list[int], status_reason: str, comments: str | None = None, company_id: str | None = None, update_related_finding_status: bool | None = None, email: str | None = None, ) -> dict[str, Any]: """Change status of alarms""" if status_reason not in STATUS_REASON_MAP: raise ValueError(f"Invalid status reason: {status_reason}") effective_company_id = company_id or self.company_id if not effective_company_id: raise ValueError("company_id must be provided either as a parameter or set on the client") url_suffix = f"/company/{effective_company_id}/alarms/status/change" json_data = { "alarm_ids": [str(aid) for aid in alarm_ids], "status": STATUS_REASON_MAP[status_reason], "comments": comments or "", } if update_related_finding_status is not None: json_data["update_related_finding_status"] = update_related_finding_status if email is not None: json_data["email"] = email response = self._http_request( method="POST", url_suffix=url_suffix, json_data=json_data, headers=self._get_headers(), timeout=60, ) if not response.get("is_success"): raise DemistoException(f"API Error: {response.get('message')}") return response def add_alarm_comment( self, alarm_id: int, user_email: str, comment: str, company_id: str | None = None, ) -> dict[str, Any]: """Add comment to an alarm""" effective_company_id = company_id or self.company_id if not effective_company_id: raise ValueError("company_id must be provided either as a parameter or set on the client") url_suffix = f"/company/{effective_company_id}/alarm/add/comment/v2" json_data = {"alarm_id": alarm_id, "user_email": user_email, "comment": comment} return self._http_request( method="POST", url_suffix=url_suffix, json_data=json_data, headers=self._get_headers(), timeout=60, ) def add_alarm_assignee( self, alarm_id: int, user_ids: list[int] | None = None, user_emails: list[str] | None = None, company_id: str | None = None, ) -> dict[str, Any]: """Add assignee(s) to an alarm""" effective_company_id = company_id or self.company_id if not effective_company_id: raise ValueError("company_id must be provided either as a parameter or set on the client") url_suffix = f"/company/{effective_company_id}/alarm/{alarm_id}/assignee" json_data: dict[str, Any] = {} if user_ids: json_data["user_ids"] = user_ids if user_emails: json_data["user_emails"] = user_emails return self._http_request( method="POST", url_suffix=url_suffix, json_data=json_data, headers=self._get_headers(), timeout=60, ) def add_remove_tag(self, alarm_id: int, tag: str, company_id: str | None = None) -> dict[str, Any]: """Add or remove a tag from an alarm""" effective_company_id = company_id or self.company_id if not effective_company_id: raise ValueError("company_id must be provided either as a parameter or set on the client") url_suffix = f"/company/{effective_company_id}/alarm/tag" json_data = {"alarm_id": alarm_id, "tag": tag} return self._http_request( method="POST", url_suffix=url_suffix, json_data=json_data, headers=self._get_headers(), timeout=60, ) def test_module(client: Client) -> str: """ Test API connectivity and credentials. Uses a narrow time window (last 1 day) and disables total record counting to avoid timeout issues on large datasets. """ try: demisto.debug("[SOCRadar] Running test module...") start_date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") demisto.debug(f"[SOCRadar] Test module using start_date: {start_date}") response = client.search_incidents(limit=1, page=1, start_date=start_date) demisto.debug(f"[SOCRadar] Test response: {response}") if response.get("is_success"): demisto.debug("[SOCRadar] Test module successful") return "ok" else: error_msg = response.get("message", "Unknown error") demisto.error(f"[SOCRadar] Test module failed: {error_msg}") return f"Test failed: {error_msg}" except DemistoException as e: error_str = str(e) demisto.error(f"[SOCRadar] Test module error: {error_str}") if "401" in error_str or "Unauthorized" in error_str: return "Authorization Error: Invalid API Key" elif "403" in error_str or "Forbidden" in error_str: return "Access Denied: Check API Key permissions and Company ID" elif "404" in error_str: return "API Endpoint Not Found: Check Company ID" else: return f"Connection failed: {error_str}" except Exception as e: demisto.error(f"[SOCRadar] Unexpected test error: {str(e)}") return f"Unexpected error: {str(e)}" # FIX: Completely rewritten parse_alarm_date. Previously each try/except block # could overwrite a successful parse from a prior block, and the final fallback # referenced an undefined variable 'e'. Now uses early-return on first successful # parse and catches only ValueError/IndexError instead of bare except. def parse_alarm_date(date_str: str | None) -> datetime | None: """Parse alarm date with multiple format support""" if not date_str: return None formats = [ ("%Y-%m-%d %H:%M:%S", 19), ("%Y-%m-%dT%H:%M:%S.%f", 26), ("%Y-%m-%dT%H:%M:%S", 19), ("%Y-%m-%d", 10), ] for fmt, slice_end in formats: try: return datetime.strptime(date_str[:slice_end], fmt) except (ValueError, IndexError): continue demisto.debug(f"[SOCRadar] Could not parse date: '{date_str}'") return None def format_value(value, indent=0): lines = [] prefix = " " * indent if isinstance(value, dict): for k, v in value.items(): if isinstance(v, dict | list): lines.append(f"{prefix}{k}:") lines.extend(format_value(v, indent + 1)) else: lines.append(f"{prefix}{k}: {v}") elif isinstance(value, list): for _, item in enumerate(value): if isinstance(item, dict | list): lines.append(f"{prefix}-") lines.extend(format_value(item, indent + 1)) else: lines.append(f"{prefix}- {item}") else: lines.append(f"{prefix}{value}") return lines # FIX: Added show_content parameter. Previously the function referenced a # variable 'show_content' that was never defined in scope, causing a NameError # at runtime. Now it is explicitly passed in from the YAML checkbox parameter. def alarm_to_incident( alarm: dict[str, Any], show_content: bool = True, include_mitigation: bool = False, include_response: bool = False, include_detection_and_analysis: bool = False, include_post_incident_analysis: bool = False, include_related_assets: bool = False, include_related_entities: bool = False, include_company_id: bool = False, include_incident_link: bool = False, configured_company_id: str = "", ) -> dict[str, Any]: """ Convert SOCRadar alarm to Demisto incident Args: alarm: Raw alarm dict from the SOCRadar API show_content: Whether to include full alarm content in incident details (controlled by the show_content checkbox in YAML config) include_mitigation: Whether to include mitigation plan in custom fields include_response: Whether to include response steps in custom fields include_detection_and_analysis: Whether to include detection & analysis in custom fields include_post_incident_analysis: Whether to include post-incident analysis in custom fields include_related_assets: Whether to include related assets in custom fields include_related_entities: Whether to include related entities in custom fields include_company_id: Whether to include company ID in custom fields configured_company_id: Company ID from integration configuration (fallback when alarm has no company_id) IMPORTANT: The 'content' field structure varies by alarm type: - Impersonating Domain: has dns_information, whois_information, domain_status - Stolen Credentials: has credential_details, log_content_link, asset_match - Bad Reputation: may have different fields - Each alarm type has its own unique content structure We safely extract common fields and include full content in rawJSON. """ company_id = alarm.get("company_id") or configured_company_id alarm_id = alarm.get("alarm_id") alarm_risk_level = alarm.get("alarm_risk_level", "UNKNOWN") alarm_asset = alarm.get("alarm_asset", "N/A") alarm_status = alarm.get("status", "UNKNOWN") alarm_type_details = alarm.get("alarm_type_details", {}) if not isinstance(alarm_type_details, dict): alarm_type_details = {} alarm_main_type = alarm_type_details.get("alarm_main_type", "Unknown") alarm_sub_type = alarm_type_details.get("alarm_sub_type", "") date_str = alarm.get("date") occurred_time = parse_alarm_date(date_str) content = alarm.get("content", {}) if not isinstance(content, dict): content = {} tags = alarm.get("tags", []) if not isinstance(tags, list): tags = [] tags_str = ",".join(str(tag) for tag in tags) incident_name = f"SOCRadar Alarm {alarm_id}: {alarm_main_type}" if alarm_sub_type: incident_name += f" - {alarm_sub_type}" incident_name += f" [{alarm_asset}]" # Build related entities string related_entities = alarm.get("alarm_related_entities", []) if not isinstance(related_entities, list): related_entities = [] entity_info = [] for entity in related_entities: if isinstance(entity, dict): key = entity.get("key", "") value = entity.get("value", "") if key and value: entity_info.append(f"{key}: {value}") related_entities_str = "\n".join(entity_info) # Build related assets string related_assets = alarm.get("alarm_related_assets", []) if not isinstance(related_assets, list): related_assets = [] asset_info = [] for asset in related_assets: if isinstance(asset, dict): asset_key = asset.get("key", "") asset_value = asset.get("value", "") if asset_key and asset_value: if isinstance(asset_value, list): asset_value = [str(v) for v in asset_value if v] if asset_value: asset_info.append(f"{asset_key}: {' || '.join(asset_value)}") else: asset_info.append(f"{asset_key}: {asset_value}") related_assets_str = "\n".join(asset_info) # Extract enrichment fields from alarm_type_details alarm_mitigation = alarm_type_details.get("alarm_default_mitigation_plan", "") alarm_response = alarm.get("alarm_response", "") alarm_detection_analysis = alarm_type_details.get("alarm_detection_and_analysis", "") alarm_post_incident = alarm_type_details.get("alarm_post_incident_analysis", "") # Promote to top-level rawJSON only when include is enabled if include_mitigation and alarm_mitigation: alarm["alarm_mitigation"] = alarm_mitigation if include_response and alarm_response: alarm["alarm_response_plan"] = alarm_response if include_detection_and_analysis and alarm_detection_analysis: alarm["alarm_detection_and_analysis"] = alarm_detection_analysis if include_post_incident_analysis and alarm_post_incident: alarm["alarm_post_incident_analysis"] = alarm_post_incident # Build incident content string from content dict incident_content = "" if show_content and content: try: content_parts = format_value(content) incident_content = "\n".join(content_parts) except Exception as e: incident_content = f"Content parsing error: {str(e)}" # Build incident link incident_link = "" if company_id and alarm_id: incident_link = ( f"https://platform.socradar.com/app/company/{company_id}" f"/alarm-management?tab=approved&field=alarmId&operator=equals&value={alarm_id}" ) alarm_text = alarm.get("alarm_text", "") details_parts = [] details_parts.append(f"Alarm ID: {alarm_id}") details_parts.append(f"Risk Level: {alarm_risk_level}") details_parts.append(f"Asset: {alarm_asset}") details_parts.append(f"Status: {alarm_status}") details_parts.append(f"Company ID: {company_id}") if alarm_main_type: details_parts.append(f"Type: {alarm_main_type}") if alarm_sub_type: details_parts.append(f" Sub-Type: {alarm_sub_type}") if entity_info: details_parts.append("\n Related Entities:") for info in entity_info: # FIX: Replaced Unicode bullet with ASCII dash. # XSOAR uses Latin-1 encoding for HTTP responses which cannot # encode U+2022, causing 'latin-1 codec can't encode character' error. details_parts.append(f" - {info}") if alarm_text: details_parts.append("\n Alarm Description:") details_parts.append(alarm_text[:3072]) if show_content and content is not None: details_parts.append("\nAlarm Content:") try: details_parts.extend(format_value(content)) except Exception as e: details_parts.append(f"Content parsing error: {str(e)}") if tags: details_parts.append(f"\n Tags: {', '.join(str(tag) for tag in tags)}") full_details = "\n".join(details_parts) custom_fields: dict[str, Any] = { "socradaralarmid": str(alarm_id) if alarm_id else "unknown", "socradarstatus": alarm_status, "socradarasset": alarm_asset, "socradaralarmtype": alarm_main_type, "socradartags": tags_str, } if include_mitigation and alarm_mitigation: custom_fields["socradarmitigation"] = alarm_mitigation if include_response and alarm_response: custom_fields["socradarresponse"] = alarm_response if include_detection_and_analysis and alarm_detection_analysis: custom_fields["socradardetectionandanalysis"] = alarm_detection_analysis if include_post_incident_analysis and alarm_post_incident: custom_fields["socradarpostincidentanalysis"] = alarm_post_incident if include_related_assets and related_assets_str: custom_fields["socradarrelatedassets"] = related_assets_str if include_related_entities and related_entities_str: custom_fields["socradarrelatedentities"] = related_entities_str if include_company_id: if "company_id" not in alarm: alarm["company_id"] = str(company_id) if company_id else "" custom_fields["socradarcompanyid"] = str(company_id) if company_id else "" if include_incident_link: if incident_link: alarm["incident_link"] = incident_link custom_fields["socradarincidentlink"] = incident_link custom_fields["socradarincidentcontent"] = incident_content incident = { "name": incident_name, "occurred": (occurred_time.isoformat() + "Z" if occurred_time else datetime.now().isoformat() + "Z"), "rawJSON": json.dumps(alarm), "severity": convert_to_demisto_severity(alarm_risk_level), "details": full_details, "dbotMirrorId": str(alarm_id) if alarm_id else None, "CustomFields": custom_fields, } demisto.debug(f"[SOCRadar] Created incident: Alarm {alarm_id} - {alarm_main_type} (Risk: {alarm_risk_level})") return incident # FIX: Added show_content parameter to fetch_incidents so it can be passed # through to alarm_to_incident from the YAML checkbox configuration. def fetch_incidents( client: Client, max_results: int, last_run: dict[str, Any], first_fetch_time: str, fetch_interval_minutes: int = 1, show_content: bool = True, include_mitigation: bool = False, include_response: bool = False, include_detection_and_analysis: bool = False, include_post_incident_analysis: bool = False, include_related_assets: bool = False, include_related_entities: bool = False, include_company_id: bool = False, include_incident_link: bool = False, configured_company_id: str = "", status: list[str] | None = None, severities: list[str] | None = None, alarm_main_types: list[str] | None = None, excluded_alarm_main_types: list[str] | None = None, alarm_sub_types: list[str] | None = None, excluded_alarm_sub_types: list[str] | None = None, alarm_type_ids: list[int] | None = None, excluded_alarm_type_ids: list[int] | None = None, ) -> tuple[dict[str, Any], list[dict[str, Any]]]: """ Fetch incidents from SOCRadar with time-window based fetching Args: client: SOCRadar API client max_results: Maximum number of incidents to fetch last_run: Last run information from previous fetch first_fetch_time: Time range for first fetch (e.g., "30 days") fetch_interval_minutes: Time window for subsequent fetches in minutes show_content: Whether to include full alarm content in incident details status: Filter by status (OPEN, CLOSED, ON_HOLD) severities: List of severity levels to filter alarm_main_types: List of main alarm types to filter alarm_sub_types: List of alarm subtypes to filter alarm_type_ids: List of alarm type IDs to include excluded_alarm_type_ids: List of alarm type IDs to exclude excluded_alarm_sub_types: List of alarm sub types to exclude excluded_alarm_main_types: List of alarm main types to exclude Strategy: - First fetch: Use first_fetch_time (e.g., "30 days ago") - Subsequent fetches: Use fetch_interval_minutes (e.g., last 1 minute) - Fetch ALL pages for the time window - Each page sent to XSOAR immediately (max 100 alarms per page) Example: First fetch (first_fetch_time = "30 days"): start_date: 30 days ago end_date: now Fetch ALL pages, each page → XSOAR Subsequent fetches (fetch_interval = 1 minute): start_date: 1 minute ago end_date: now If 500 new alarms → 5 pages → 5 separate sends to XSOAR """ demisto.debug("[SOCRadar] Starting fetch_incidents") demisto.debug(f"[SOCRadar] max_results: {max_results}") demisto.debug(f"[SOCRadar] fetch_interval_minutes: {fetch_interval_minutes}") demisto.debug(f"[SOCRadar] alarm_type_ids: {alarm_type_ids}") demisto.debug(f"[SOCRadar] excluded_alarm_type_ids: {excluded_alarm_type_ids}") demisto.debug(f"[SOCRadar] excluded_alarm_sub_types: {excluded_alarm_sub_types}") demisto.debug(f"[SOCRadar] excluded_alarm_main_types: {excluded_alarm_main_types}") # FIX: Renamed to last_fetch_str to avoid variable name collision. # Previously 'last_fetch' was reused later as a datetime object which # shadowed this string value and could cause confusion. last_fetch_str = last_run.get("last_fetch") last_alarm_ids = set(last_run.get("last_alarm_ids", [])) last_alarm_ids_backup = last_alarm_ids.copy() current_time = datetime.now() demisto.debug(f"[SOCRadar] Last fetch: {last_fetch_str}") if last_fetch_str: try: start_datetime = datetime.fromisoformat(last_fetch_str) - timedelta(minutes=fetch_interval_minutes) demisto.debug(f"[SOCRadar] Subsequent fetch: Using last_fetch datetime {start_datetime.isoformat()}") except Exception: start_datetime = current_time - timedelta(minutes=fetch_interval_minutes) demisto.debug("[SOCRadar] Failed to parse last_fetch, fallback to interval") else: start_datetime_temp = arg_to_datetime(first_fetch_time, arg_name="first_fetch", required=True) if not start_datetime_temp: raise ValueError("Failed to parse first_fetch_time") start_datetime = start_datetime_temp demisto.debug("[SOCRadar] First fetch: Using first_fetch_time") start_date = start_datetime.isoformat() end_date = current_time.isoformat() demisto.debug(f"[SOCRadar] Time window: {start_date} to {end_date}") demisto.debug("[SOCRadar] Will fetch ALL pages for this time window") all_incidents = [] reached_max_results = False per_page = 100 latest_page = last_run.get("latest_page", 1) current_page = latest_page next_run_incidents_to_skip = last_run.get("next_run_incidents_to_skip", 0) total_pages = None total_incidents_created = 0 total_pages_fetched = 0 try: while True: demisto.debug(f"[SOCRadar] Fetching page {current_page}/{total_pages if total_pages else '?'}") response = client.search_incidents( status=status, severities=severities, alarm_main_types=alarm_main_types, alarm_sub_types=alarm_sub_types, alarm_type_ids=alarm_type_ids, excluded_alarm_type_ids=excluded_alarm_type_ids, excluded_alarm_main_types=excluded_alarm_main_types, excluded_alarm_sub_types=excluded_alarm_sub_types, start_date=start_date, end_date=end_date, limit=per_page, page=current_page, include_company_id=include_company_id, ) alarms = response.get("data", []) total_records = response.get("total_records", 0) total_pages = response.get("total_pages", 0) demisto.debug(f"[SOCRadar] Page {current_page}: Received {len(alarms)} alarms") if current_page == 1: demisto.debug(f"[SOCRadar] Total available in time window: {total_records} records across {total_pages} pages") demisto.debug(f"[SOCRadar] Will fetch ALL {total_pages} pages") total_pages_fetched += 1 if not alarms: demisto.debug(f"[SOCRadar] No alarms on page {current_page}") break page_incidents = [] page_new = 0 page_dup = 0 for alarm in alarms[next_run_incidents_to_skip:]: if alarm.get("alarm_id") in last_alarm_ids: page_dup += 1 continue if total_incidents_created < max_results: # FIX: Pass show_content parameter to alarm_to_incident incident = alarm_to_incident( alarm, show_content=show_content, include_mitigation=include_mitigation, include_response=include_response, include_detection_and_analysis=include_detection_and_analysis, include_post_incident_analysis=include_post_incident_analysis, include_related_assets=include_related_assets, include_related_entities=include_related_entities, include_company_id=include_company_id, include_incident_link=include_incident_link, configured_company_id=configured_company_id, ) page_incidents.append(incident) total_incidents_created += 1 last_alarm_ids.add(alarm.get("alarm_id")) page_new += 1 demisto.debug(f"[SOCRadar] Page {current_page}: Created {page_new} incidents, skipped {page_dup} duplicates") all_incidents.extend(page_incidents) if total_incidents_created >= max_results: reached_max_results = True if current_page == latest_page: next_run_incidents_to_skip += len(page_incidents) else: next_run_incidents_to_skip = len(page_incidents) demisto.debug( f"[SOCRadar] Reached max_results ({max_results}), fetch pages is stopped. " f"Next run will start from the date of the latest run with skipping {next_run_incidents_to_skip} incidents" ) break next_run_incidents_to_skip = 0 if current_page >= total_pages: demisto.debug(f"[SOCRadar] Reached last page ({current_page}/{total_pages})") break if len(alarms) < per_page: demisto.debug(f"[SOCRadar] Partial page ({len(alarms)} < {per_page}), probably last page") break current_page += 1 demisto.debug("[SOCRadar] ========== FETCH SUMMARY ==========") demisto.debug(f"[SOCRadar] Time window: {start_date} to {end_date}") demisto.debug(f"[SOCRadar] Fetch interval: {fetch_interval_minutes} minutes") demisto.debug(f"[SOCRadar] Pages fetched: {total_pages_fetched}/{total_pages if total_pages else 'unknown'}") demisto.debug(f"[SOCRadar] Incidents created: {total_incidents_created} (max: {max_results})") demisto.debug(f"[SOCRadar] Alarm Type IDs filter: {alarm_type_ids}") demisto.debug(f"[SOCRadar] Excluded Alarm Type IDs: {excluded_alarm_type_ids}") demisto.debug(f"[SOCRadar] Excluded Alarm Sub Types: {excluded_alarm_sub_types}") demisto.debug(f"[SOCRadar] Excluded Alarm Main Types: {excluded_alarm_main_types}") demisto.debug("[SOCRadar] ====================================") # FIX: Use a new variable name 'next_fetch_time' to avoid shadowing # the 'last_fetch_str' string from the top of the function. if reached_max_results: next_fetch_time = start_datetime else: next_fetch_time = current_time + timedelta(seconds=1) combined = list(last_alarm_ids)[:10000] next_run = { "last_fetch": next_fetch_time.isoformat(), "start_date": start_date, "end_date": end_date, "next_run_incidents_to_skip": next_run_incidents_to_skip, "latest_page": current_page if next_run_incidents_to_skip else 1, "last_alarm_ids": combined, } demisto.debug(f"[SOCRadar] Returning {len(all_incidents)} incidents to XSOAR") return next_run, all_incidents except Exception as e: demisto.error(f"[SOCRadar] Error in fetch_incidents: {str(e)}") demisto.error(f"[SOCRadar] Traceback: {traceback.format_exc()}") return { "last_alarm_ids": list(last_alarm_ids_backup)[:10000], "last_fetch": start_date, "start_date": start_date, "end_date": end_date, "exception": str(e), }, [] def change_status_command(client: Client, args: dict[str, str]) -> CommandResults: """Change status of alarms""" alarm_ids_str = args.get("alarm_ids", "") status_reason = args.get("status_reason", "") comments = args.get("comments") company_id = args.get("company_id") update_related_finding_status_str = args.get("update_related_finding_status") update_related_finding_status: bool | None = ( argToBoolean(update_related_finding_status_str) if update_related_finding_status_str else None ) email = args.get("email") if not alarm_ids_str or not status_reason: raise ValueError("alarm_ids and status_reason are required") if update_related_finding_status and not email: raise ValueError("Argument 'email' is required when 'update_related_finding_status' is set to true.") alarm_ids = [int(aid.strip()) for aid in alarm_ids_str.split(",")] response = client.change_alarm_status( alarm_ids, status_reason, comments, company_id, update_related_finding_status, email, ) return CommandResults( readable_output=f"Status changed for {len(alarm_ids)} alarm(s)", raw_response=response, ) def mark_as_false_positive_command(client: Client, args: dict[str, str]) -> CommandResults: """Mark alarm as false positive""" alarm_id = args.get("alarm_id") company_id = args.get("company_id") if not alarm_id: raise ValueError("alarm_id is required") response = client.change_alarm_status( [int(alarm_id)], "FALSE_POSITIVE", args.get("comments", "Marked as false positive"), company_id, ) return CommandResults( readable_output=f"Alarm {alarm_id} marked as false positive", raw_response=response, ) def mark_as_resolved_command(client: Client, args: dict[str, str]) -> CommandResults: """Mark alarm as resolved""" alarm_id = args.get("alarm_id") company_id = args.get("company_id") if not alarm_id: raise ValueError("alarm_id is required") response = client.change_alarm_status( [int(alarm_id)], "RESOLVED", args.get("comments", "Marked as resolved"), company_id, ) return CommandResults(readable_output=f"Alarm {alarm_id} marked as resolved", raw_response=response) def add_comment_command(client: Client, args: dict[str, str]) -> CommandResults: """Add comment to alarm""" alarm_id = arg_to_number(args.get("alarm_id"), "alarm_id", required=True) company_id = args.get("company_id") if alarm_id is None: raise ValueError("alarm_id is required") user_email = args.get("user_email", "") comment = args.get("comment", "") if not user_email or not comment: raise ValueError("user_email and comment are required") response = client.add_alarm_comment(alarm_id, user_email, comment, company_id) return CommandResults(readable_output=f"Comment added to alarm {alarm_id}", raw_response=response) def add_assignee_command(client: Client, args: dict[str, str]) -> CommandResults: """Add assignee(s) to an alarm.""" alarm_id = arg_to_number(args.get("alarm_id"), "alarm_id", required=True) company_id = args.get("company_id") if alarm_id is None: raise ValueError("alarm_id is required") user_emails = argToList(args.get("user_emails")) if not user_emails: raise ValueError("user_emails is required") response = client.add_alarm_assignee(alarm_id, user_emails=user_emails, company_id=company_id) return CommandResults(readable_output=f"Assignee added for alarm {alarm_id}", raw_response=response) def add_tag_command(client: Client, args: dict[str, str]) -> CommandResults: """Add or remove tag from alarm""" alarm_id = arg_to_number(args.get("alarm_id"), "alarm_id", required=True) company_id = args.get("company_id") if alarm_id is None: raise ValueError("alarm_id is required") tag = args.get("tag", "") if not tag: raise ValueError("tag is required") response = client.add_remove_tag(alarm_id, tag, company_id) return CommandResults( readable_output=f"Tag '{tag}' added/removed for alarm {alarm_id}", raw_response=response, ) def test_fetch_command(client: Client, args: dict[str, str]) -> CommandResults: """ Test incident fetching with safe handling of variable content structures. Disables total record counting for faster response times. Shows sample incidents to verify API connectivity and data format. """ limit = arg_to_number(args.get("limit", "5"), "limit") or 5 try: first_fetch = args.get("first_fetch", "3 days") first_fetch_datetime = arg_to_datetime(first_fetch, arg_name="first_fetch", required=True) if not first_fetch_datetime: raise ValueError("Failed to parse first_fetch") start_date = first_fetch_datetime.strftime("%Y-%m-%d") demisto.debug(f"[SOCRadar Test] Testing fetch from {start_date}") # FIX: include_total_records is now a proper parameter of search_incidents. # Previously passing it as a kwarg caused TypeError because the method # did not have this in its signature. response = client.search_incidents( limit=limit, start_date=start_date, page=1, include_total_records=False, ) data = response.get("data", []) if not data: message = "No incidents found. Possible reasons:\n" message += f"- No active alarms in SOCRadar from {start_date}\n" message += "- Filters are too restrictive\n" message += "- Date range is too narrow\n\n" message += f"Tested with start_date: {start_date}\n" return CommandResults(readable_output=message, raw_response=response) incidents_info = [] for incident in data[:5]: alarm_type_details = incident.get("alarm_type_details") if isinstance(alarm_type_details, dict): main_type = alarm_type_details.get("alarm_main_type", "Unknown") sub_type = alarm_type_details.get("alarm_sub_type", "") else: main_type = "Unknown" sub_type = "" alarm_type_display = main_type if sub_type: alarm_type_display += f" / {sub_type}" related_entities = incident.get("alarm_related_entities", []) entity_summary = "" if isinstance(related_entities, list) and related_entities: first_entity = related_entities[0] if isinstance(first_entity, dict): entity_value = first_entity.get("value", "") if entity_value: entity_summary = f" | Entity: {entity_value[:30]}" incidents_info.append( { "Alarm ID": incident.get("alarm_id", "N/A"), "Risk Level": incident.get("alarm_risk_level", "UNKNOWN"), "Status": incident.get("status", "UNKNOWN"), "Asset": incident.get("alarm_asset", "N/A"), "Type": alarm_type_display, "Date": (incident.get("date", "")[:19] if incident.get("date") else "N/A"), "Extra": entity_summary, } ) message = f"Found {len(data)} incident(s) on page 1 from {start_date}!\n\n" message += "Sample incidents:\n" for info in incidents_info: message += f"- [{info['Alarm ID']}] {info['Risk Level']} | {info['Status']} | {info['Asset']}\n" message += f" Type: {info['Type']}{info['Extra']}\n" if data: first_alarm = data[0] content = first_alarm.get("content") if isinstance(content, dict): content_keys = list(content.keys()) message += f"\n📋 Content structure example (keys): {', '.join(content_keys[:5])}" if len(content_keys) > 5: message += f" ... and {len(content_keys) - 5} more" return CommandResults( readable_output=message, outputs_prefix="SOCRadar.TestFetch", outputs={ "TotalCount": len(data), "SampleIncidents": incidents_info, "StartDate": start_date, }, raw_response=response, ) except Exception as e: error_msg = str(e) message = f"Error testing fetch: {error_msg}\n\n" message += "Check:\n" message += "- API key validity\n" message += "- Network connectivity\n" message += "- Company ID correctness\n" message += f"- Date parsing (tried to parse: '{args.get('first_fetch', '3 days')}')\n\n" message += f"Full error:\n{traceback.format_exc()}" return CommandResults( readable_output=message, raw_response={"error": error_msg, "traceback": traceback.format_exc()}, ) def main() -> None: """Main execution function""" params = demisto.params() api_key_param = params.get("apikey") if isinstance(api_key_param, dict): api_key = api_key_param.get("password", "") else: api_key = api_key_param company_id = params.get("company_id") verify_certificate = not params.get("insecure", False) proxy = params.get("proxy", False) demisto.debug(f"[SOCRadar] Starting command: {demisto.command()}") demisto.debug(f"[SOCRadar] Company ID: {company_id}") try: client = Client( base_url=SOCRADAR_API_ENDPOINT, api_key=api_key, company_id=company_id, verify=verify_certificate, proxy=proxy, ) command = demisto.command() if command == "test-module": return_results(test_module(client)) elif command == "fetch-incidents": max_fetch = arg_to_number(params.get("max_fetch", 100000)) or 100000 max_fetch = min(max_fetch, MAX_INCIDENTS_TO_FETCH) fetch_interval_minutes = arg_to_number(params.get("fetch_interval_minutes", 1)) or 1 # FIX: Read show_content from YAML checkbox parameter and pass it # through to fetch_incidents → alarm_to_incident. Previously the # show_content variable was referenced inside alarm_to_incident but # never defined, causing NameError. show_content = params.get("show_content", True) if isinstance(show_content, str): show_content = show_content.lower() in ("true", "1", "yes") include_mitigation = argToBoolean(params.get("include_mitigation", False)) include_response = argToBoolean(params.get("include_response", False)) include_detection_and_analysis = argToBoolean(params.get("include_detection_and_analysis", False)) include_post_incident_analysis = argToBoolean(params.get("include_post_incident_analysis", False)) include_related_assets = argToBoolean(params.get("include_related_assets", False)) include_related_entities = argToBoolean(params.get("include_related_entities", False)) include_company_id = argToBoolean(params.get("include_company_id", False)) include_incident_link = argToBoolean(params.get("include_incident_link", False)) alarm_type_ids_str = params.get("alarm_type_ids", "") alarm_type_ids = None if alarm_type_ids_str: try: alarm_type_ids = [int(x.strip()) for x in alarm_type_ids_str.split(",") if x.strip()] except ValueError: demisto.error(f"[SOCRadar] Invalid alarm_type_ids format: {alarm_type_ids_str}") excluded_alarm_type_ids_str = params.get("excluded_alarm_type_ids", "") excluded_alarm_type_ids = None if excluded_alarm_type_ids_str: try: # FIX: Parse excluded_alarm_type_ids as integers instead of strings. # Previously this used [x.strip() for x in ...] which produced # string values, inconsistent with alarm_type_ids (int list) # and the API expectation of integer IDs. excluded_alarm_type_ids = [int(x.strip()) for x in excluded_alarm_type_ids_str.split(",") if x.strip()] except ValueError: demisto.error(f"[SOCRadar] Invalid excluded_alarm_type_ids format: {excluded_alarm_type_ids_str}") excluded_alarm_sub_types_str = params.get("excluded_alarm_sub_types", "") excluded_alarm_sub_types = None if excluded_alarm_sub_types_str: try: excluded_alarm_sub_types = [x.strip() for x in excluded_alarm_sub_types_str.split(",") if x.strip()] except ValueError: demisto.error(f"[SOCRadar] Invalid excluded_alarm_sub_types format: {excluded_alarm_sub_types_str}") excluded_alarm_main_types_str = params.get("excluded_alarm_main_types", "") excluded_alarm_main_types = None if excluded_alarm_main_types_str: try: excluded_alarm_main_types = [x.strip() for x in excluded_alarm_main_types_str.split(",") if x.strip()] except ValueError: demisto.error(f"[SOCRadar] Invalid excluded_alarm_main_types format: {excluded_alarm_main_types_str}") demisto.debug( f"[SOCRadar] Fetch config - max_fetch: {max_fetch}, " f"first_fetch: {params.get('first_fetch')}, " f"fetch_interval: {fetch_interval_minutes} minutes" ) demisto.debug( f"[SOCRadar] Fetch config - alarm_type_ids: {alarm_type_ids}, " f"excluded_alarm_type_ids: {excluded_alarm_type_ids}, " f"excluded_alarm_main_types: {excluded_alarm_main_types}, " f"excluded_alarm_sub_types: {excluded_alarm_sub_types}" ) next_run, incidents = fetch_incidents( client=client, max_results=max_fetch, last_run=demisto.getLastRun(), first_fetch_time=params.get("first_fetch", "3 days"), fetch_interval_minutes=fetch_interval_minutes, show_content=show_content, include_mitigation=include_mitigation, include_response=include_response, include_detection_and_analysis=include_detection_and_analysis, include_post_incident_analysis=include_post_incident_analysis, include_related_assets=include_related_assets, include_related_entities=include_related_entities, include_company_id=include_company_id, include_incident_link=include_incident_link, configured_company_id=company_id or "", status=argToList(params.get("status")), severities=argToList(params.get("severities")), alarm_main_types=argToList(params.get("alarm_main_types")), alarm_sub_types=argToList(params.get("alarm_sub_types")), alarm_type_ids=alarm_type_ids, excluded_alarm_type_ids=excluded_alarm_type_ids, excluded_alarm_sub_types=excluded_alarm_sub_types, excluded_alarm_main_types=excluded_alarm_main_types, ) demisto.debug(f"[SOCRadar] Setting last run to: {next_run}") demisto.debug(f"[SOCRadar] Returning {len(incidents)} incidents") demisto.setLastRun(next_run) demisto.incidents(incidents) elif command == "socradar-change-alarm-status": return_results(change_status_command(client, demisto.args())) elif command == "socradar-mark-false-positive": return_results(mark_as_false_positive_command(client, demisto.args())) elif command == "socradar-mark-resolved": return_results(mark_as_resolved_command(client, demisto.args())) elif command == "socradar-add-comment": return_results(add_comment_command(client, demisto.args())) elif command == "socradar-add-assignee" or command == "socradar-change-assignee": return_results(add_assignee_command(client, demisto.args())) elif command == "socradar-add-tag": return_results(add_tag_command(client, demisto.args())) elif command == "socradar-test-fetch": return_results(test_fetch_command(client, demisto.args())) else: raise NotImplementedError(f"Command {command} is not implemented") except Exception as e: demisto.error(f"[SOCRadar] Error: {str(e)}") demisto.error(f"[SOCRadar] Traceback: {traceback.format_exc()}") return_error(f"Failed to execute {demisto.command()}.\nError: {str(e)}") if __name__ in ("__main__", "__builtin__", "builtins"): main()