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

IDSOCRadarIncidentsV4
ProviderSOCRadar
CategoryAnalytics & SIEM
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10116658
Supported ModulesAgentix 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:

  1. Log in to SOCRadar Platform
  2. Navigate to Settings → API & Integrations
  3. Go to API Options page
  4. Copy your Company API Key (for Incident API)
  5. 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

  1. Navigate to Settings → Marketplace
  2. Search for “SOCRadar”
  3. Click Install
  4. 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

  1. Navigate to Settings → Integrations → Servers & Services
  2. Search for “SOCRadar Incidents v4”
  3. 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 status
    • OPEN, INVESTIGATING, RESOLVED, PENDING_INFO
    • LEGAL_REVIEW, VENDOR_ASSESSMENT, FALSE_POSITIVE
    • DUPLICATE, PROCESSED_INTERNALLY, MITIGATED, NOT_APPLICABLE
  • comments (Optional): Status change comments
  • company_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 ID
  • comments (Optional): Reason for false positive
  • company_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 ID
  • comments (Optional): Resolution notes
  • company_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 ID
  • user_email (Required): Email of user posting comment
  • comment (Required): Comment text
  • company_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 ID
  • comment (Required): Message for analyst
  • company_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 ID
  • user_emails (Required): Comma-separated email addresses
  • company_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 ID
  • tag (Required): Tag name
  • company_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 ID
  • severity (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 ID
  • socradarstatus: Current status
  • socradarasset: Affected asset
  • socradaralarmtype: Main alarm type
  • socradaralarmsubtype: Alarm sub-type
  • socradaralarmtypeid: Type ID
  • socradartags: Comma-separated tags
  • socradarrisklevel: Risk level
  • socradaralarmtext: 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_information
  • socradarcontentwhois_information
  • socradarcontentdomain_status

Stolen Credentials:

  • socradarcontentcredential_details
  • socradarcontentlog_content_link
  • socradarcontentmalware_family

Data Leak:

  • socradarcontentsource_full_content
  • socradarcontentcompromised_emails
  • socradarcontentcompromised_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:

  1. Test connection: Click “Test” button
  2. Date range: Increase “First fetch time” to “7 days”
  3. Filters: Remove status/severity filters temporarily
  4. 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:

  1. Check fetch interval: Should be ≥ 1 minute
  2. Check logs for deduplication stats:

    [SOCRadar V4.0] Page 1: Created 5 NEW incidents, skipped 95 duplicates
    
  3. 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:

  1. Verify “Include Alarm Content” is enabled
  2. Create CustomFields manually in XSOAR:
    • Settings → Advanced → Fields
    • Add fields with type “Short Text”
    • Prefix: socradarcontent

If entities not appearing:

  1. 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

Feature Requests

Submit feature requests through:

  1. SOCRadar platform Request
  2. 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 incidents
  • first_fetch — First Fetch Time
  • fetch_interval_minutes — Fetch Interval (Minutes)
  • max_fetch — Max incidents per fetch
  • show_content — Show Alarm Content
  • status — Status Filter
  • severities — Severity Levels
  • alarm_type_ids — Alarm Type IDs (Include)
  • excluded_alarm_type_ids — Alarm Type IDs (Exclude)
  • alarm_main_types — Alarm Main Types
  • excluded_alarm_main_types — Alarm Main Types (Exclude)
  • alarm_sub_types — Alarm Sub Types
  • excluded_alarm_sub_types — Alarm Sub Types (Exclude)
  • include_company_id — Include Company ID
  • include_incident_link — Include Incident Link
  • 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
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • incidentType — Incident type
  • incidentFetchInterval — Incidents Fetch Interval

Commands (8)

  • socradar-add-assignee

    add the assignee(s) of an alarm.

  • socradar-add-comment

    Add a comment to an alarm.

  • socradar-add-tag

    Add or remove a tag from an alarm.

  • socradar-change-alarm-status

    Change the status of one or more alarms.

  • socradar-change-assignee Deprecated

    add the assignee(s) of an alarm.

  • socradar-mark-false-positive

    Mark an alarm as false positive.

  • socradar-mark-resolved

    Mark an alarm as resolved.

  • socradar-test-fetch

    Test incident fetching to verify alarms are available and date parsing works correctly.

import pytest
from datetime import datetime, timedelta

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_alarm(
    alarm_id=123,
    risk="HIGH",
    asset="TESTSOCRadar",
    status="OPEN",
    main_type="Test Type",
    sub_type="Test Sub",
    alarm_text="Test alarm",
    date="2024-01-15T10:30:00",
    tags=None,
    related_entities=None,
    company_id="456",
):
    return {
        "alarm_id": alarm_id,
        "company_id": company_id,
        "alarm_risk_level": risk,
        "alarm_asset": asset,
        "status": status,
        "date": date,
        "alarm_type_details": {
            "alarm_main_type": main_type,
            "alarm_sub_type": sub_type,
        },
        "alarm_text": alarm_text,
        "tags": tags or [],
        "alarm_related_entities": related_entities or [],
    }


def _mock_response(alarms, total_pages=1, total_records=None):
    return {
        "is_success": True,
        "data": alarms,
        "total_records": total_records if total_records is not None else len(alarms),
        "total_pages": total_pages,
        "current_page": 1,
    }


# ---------------------------------------------------------------------------
# Fixture
# ---------------------------------------------------------------------------


@pytest.fixture
def mock_client():
    from SOCRadarIncidentsV4 import Client

    client = Client(
        base_url="https://platform.socradar.com/api",
        api_key="test-api-key",
        company_id="123",
        verify=False,
        proxy=False,
    )
    return client


# ---------------------------------------------------------------------------
# convert_to_demisto_severity
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "severity,expected_attr",
    [
        ("low", "LOW"),
        ("medium", "MEDIUM"),
        ("high", "HIGH"),
        ("critical", "CRITICAL"),
        ("info", "INFO"),
        ("unknown", "UNKNOWN"),
    ],
)
def test_convert_to_demisto_severity(severity, expected_attr):
    from SOCRadarIncidentsV4 import convert_to_demisto_severity
    from CommonServerPython import IncidentSeverity

    result = convert_to_demisto_severity(severity)
    assert result == getattr(IncidentSeverity, expected_attr)


# ---------------------------------------------------------------------------
# parse_alarm_date
# ---------------------------------------------------------------------------


@pytest.mark.parametrize(
    "date_str,description",
    [
        ("2024-01-15T10:30:00.123456", "with_microseconds"),
        ("2024-01-15T10:30:00", "without_microseconds"),
        ("2024-01-15", "date_only"),
    ],
)
def test_parse_alarm_date_valid(date_str, description):
    from SOCRadarIncidentsV4 import parse_alarm_date

    result = parse_alarm_date(date_str)
    assert result is not None
    assert isinstance(result, datetime)


@pytest.mark.parametrize(
    "date_str,description",
    [
        (None, "none_date"),
        ("not-a-date", "invalid_format"),
    ],
)
def test_parse_alarm_date_invalid(date_str, description):
    from SOCRadarIncidentsV4 import parse_alarm_date

    result = parse_alarm_date(date_str)
    assert result is None


# ---------------------------------------------------------------------------
# alarm_to_incident
# ---------------------------------------------------------------------------


def test_alarm_to_incident_basic():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    incident = alarm_to_incident(alarm)

    assert "SOCRadar Alarm 123" in incident["name"]
    assert "Test Type" in incident["name"]
    assert "Test Sub" in incident["name"]
    assert "TESTSOCRadar" in incident["name"]  # asset field value
    assert incident["CustomFields"]["socradaralarmid"] == "123"
    assert incident["CustomFields"]["socradarasset"] == "TESTSOCRadar"
    assert incident["CustomFields"]["socradarstatus"] == "OPEN"
    assert incident["CustomFields"]["socradaralarmtype"] == "Test Type"


def test_alarm_to_incident_with_tags_and_entities():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm(
        tags=["tag1", "tag2"],
        related_entities=[{"key": "IP", "value": "1.2.3.4"}],
    )
    incident = alarm_to_incident(alarm)

    assert "tag1,tag2" in incident["CustomFields"]["socradartags"]
    assert "IP: 1.2.3.4" in incident["details"]


def test_alarm_to_incident_missing_fields():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = {"alarm_id": 999}
    incident = alarm_to_incident(alarm)

    assert incident["name"] == "SOCRadar Alarm 999: Unknown [N/A]"
    assert incident["CustomFields"]["socradaralarmid"] == "999"


def test_alarm_to_incident_raw_json():
    import json
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    incident = alarm_to_incident(alarm)

    raw = json.loads(incident["rawJSON"])
    assert raw["alarm_id"] == 123


# ---------------------------------------------------------------------------
# Client
# ---------------------------------------------------------------------------


class TestClient:
    def test_client_initialization(self, mock_client):
        assert mock_client.api_key == "test-api-key"
        assert mock_client.company_id == "123"

    def test_get_headers(self, mock_client):
        headers = mock_client._get_headers()
        assert headers == {"API-Key": "test-api-key"}

    def test_search_incidents_params(self, mock_client, mocker):
        mock_http = mocker.patch.object(
            mock_client,
            "_http_request",
            return_value={
                "is_success": True,
                "data": {"alarms": [], "total_pages": 1, "total_records": 0},
            },
        )

        mock_client.search_incidents(limit=10, page=2, start_date="2024-01-01")

        call_kwargs = mock_http.call_args
        params = call_kwargs[1]["params"]
        assert params["limit"] == 10
        assert params["page"] == 2
        assert params["start_date"] == "2024-01-01"

    def test_change_alarm_status_invalid_status(self, mock_client):
        with pytest.raises(ValueError, match="Invalid status reason"):
            mock_client.change_alarm_status([1], "INVALID_STATUS")

    def test_change_alarm_status_success(self, mock_client, mocker):
        mocker.patch.object(
            mock_client,
            "_http_request",
            return_value={"is_success": True},
        )
        result = mock_client.change_alarm_status([1, 2], "RESOLVED")
        assert result["is_success"] is True

    def test_add_alarm_comment(self, mock_client, mocker):
        mocker.patch.object(
            mock_client,
            "_http_request",
            return_value={"is_success": True},
        )
        result = mock_client.add_alarm_comment(1, "user@test.com", "test comment")
        assert result["is_success"] is True

    def test_add_alarm_assignee(self, mock_client, mocker):
        mocker.patch.object(
            mock_client,
            "_http_request",
            return_value={"is_success": True},
        )
        result = mock_client.add_alarm_assignee(1, user_emails=["user@test.com"])
        assert result["is_success"] is True

    def test_add_remove_tag(self, mock_client, mocker):
        mocker.patch.object(
            mock_client,
            "_http_request",
            return_value={"is_success": True},
        )
        result = mock_client.add_remove_tag(1, "critical-tag")
        assert result["is_success"] is True


# ---------------------------------------------------------------------------
# test_module
# ---------------------------------------------------------------------------


def test_test_module_success(mock_client, mocker):
    from SOCRadarIncidentsV4 import test_module

    mocker.patch.object(mock_client, "search_incidents", return_value={"is_success": True, "data": []})
    result = test_module(mock_client)
    assert result == "ok"


def test_test_module_failure(mock_client, mocker, capfd):
    from SOCRadarIncidentsV4 import test_module

    mocker.patch.object(
        mock_client,
        "search_incidents",
        return_value={"is_success": False, "message": "Authentication failed"},
    )
    with capfd.disabled():
        result = test_module(mock_client)
    assert "Test failed" in result


def test_test_module_unauthorized(mock_client, mocker, capfd):
    from SOCRadarIncidentsV4 import test_module
    from CommonServerPython import DemistoException

    mocker.patch.object(
        mock_client,
        "search_incidents",
        side_effect=DemistoException("401 Unauthorized"),
    )
    with capfd.disabled():
        result = test_module(mock_client)
    assert "Authorization Error" in result


def test_test_module_forbidden(mock_client, mocker, capfd):
    from SOCRadarIncidentsV4 import test_module
    from CommonServerPython import DemistoException

    mocker.patch.object(mock_client, "search_incidents", side_effect=DemistoException("403 Forbidden"))
    with capfd.disabled():
        result = test_module(mock_client)
    assert "Access Denied" in result


def test_test_module_not_found(mock_client, mocker, capfd):
    from SOCRadarIncidentsV4 import test_module
    from CommonServerPython import DemistoException

    mocker.patch.object(mock_client, "search_incidents", side_effect=DemistoException("404 Not Found"))
    with capfd.disabled():
        result = test_module(mock_client)
    assert "Not Found" in result


# ---------------------------------------------------------------------------
# status_reason_map completeness
# ---------------------------------------------------------------------------


def test_status_reason_map_completeness():
    from SOCRadarIncidentsV4 import STATUS_REASON_MAP

    expected_statuses = [
        "OPEN",
        "INVESTIGATING",
        "RESOLVED",
        "PENDING_INFO",
        "LEGAL_REVIEW",
        "VENDOR_ASSESSMENT",
        "FALSE_POSITIVE",
        "DUPLICATE",
        "PROCESSED_INTERNALLY",
        "MITIGATED",
        "NOT_APPLICABLE",
    ]
    for status in expected_statuses:
        assert status in STATUS_REASON_MAP


# ---------------------------------------------------------------------------
# fetch_incidents
# ---------------------------------------------------------------------------


def test_fetch_incidents_first_fetch(mock_client, mocker):
    """Test fetch incidents on first run"""
    from SOCRadarIncidentsV4 import fetch_incidents

    mocker.patch.object(mock_client, "search_incidents", return_value=_mock_response([_make_alarm()]))

    next_run, incidents = fetch_incidents(
        client=mock_client,
        max_results=100,
        last_run={},
        first_fetch_time="3 days",
        fetch_interval_minutes=1,
    )

    assert len(incidents) == 1
    assert "last_fetch" in next_run
    assert "last_alarm_ids" in next_run
    assert 123 in next_run["last_alarm_ids"]


def test_fetch_incidents_subsequent_fetch(mock_client, mocker):
    """Test fetch incidents on subsequent runs uses interval"""
    from SOCRadarIncidentsV4 import fetch_incidents

    last_fetch_time = (datetime.now() - timedelta(minutes=5)).isoformat() + "Z"
    mocker.patch.object(
        mock_client,
        "search_incidents",
        return_value=_mock_response([_make_alarm(alarm_id=200)]),
    )

    next_run, incidents = fetch_incidents(
        client=mock_client,
        max_results=100,
        last_run={"last_fetch": last_fetch_time, "last_alarm_ids": []},
        first_fetch_time="3 days",
        fetch_interval_minutes=1,
    )

    assert len(incidents) == 1
    assert 200 in next_run["last_alarm_ids"]


def test_fetch_incidents_with_duplicates(mock_client, mocker):
    """Test that duplicate alarm IDs are skipped"""
    from SOCRadarIncidentsV4 import fetch_incidents

    last_fetch_time = (datetime.now() - timedelta(minutes=5)).isoformat() + "Z"
    mocker.patch.object(
        mock_client,
        "search_incidents",
        return_value=_mock_response([_make_alarm(alarm_id=123)]),
    )

    next_run, incidents = fetch_incidents(
        client=mock_client,
        max_results=100,
        last_run={"last_fetch": last_fetch_time, "last_alarm_ids": [123]},
        first_fetch_time="3 days",
        fetch_interval_minutes=1,
    )

    assert len(incidents) == 0


def test_fetch_incidents_no_alarms(mock_client, mocker):
    """Test fetch when no alarms returned"""
    from SOCRadarIncidentsV4 import fetch_incidents

    mocker.patch.object(mock_client, "search_incidents", return_value=_mock_response([]))

    next_run, incidents = fetch_incidents(
        client=mock_client,
        max_results=100,
        last_run={},
        first_fetch_time="3 days",
        fetch_interval_minutes=1,
    )

    assert len(incidents) == 0
    assert "last_fetch" in next_run
    assert "last_alarm_ids" in next_run


def test_fetch_incidents_error_handling(mock_client, mocker, capfd):
    """Test fetch gracefully handles API errors"""
    from SOCRadarIncidentsV4 import fetch_incidents

    mocker.patch.object(mock_client, "search_incidents", side_effect=Exception("API Error"))

    with capfd.disabled():
        next_run, incidents = fetch_incidents(
            client=mock_client,
            max_results=100,
            last_run={},
            first_fetch_time="3 days",
            fetch_interval_minutes=1,
        )

    assert incidents == []
    assert "last_fetch" in next_run


def test_fetch_incidents_with_filters(mock_client, mocker):
    """Test fetch passes filters correctly to search_incidents"""
    from SOCRadarIncidentsV4 import fetch_incidents

    mock_search = mocker.patch.object(mock_client, "search_incidents", return_value=_mock_response([_make_alarm()]))

    fetch_incidents(
        client=mock_client,
        max_results=100,
        last_run={},
        first_fetch_time="3 days",
        fetch_interval_minutes=1,
        status=["OPEN"],
        severities=["HIGH"],
        alarm_main_types=["Phishing"],
    )

    call_kwargs = mock_search.call_args[1]
    assert call_kwargs["status"] == ["OPEN"]
    assert call_kwargs["severities"] == ["HIGH"]
    assert call_kwargs["alarm_main_types"] == ["Phishing"]


# ---------------------------------------------------------------------------
# change_status_command
# ---------------------------------------------------------------------------


def test_change_status_command_success(mock_client, mocker):
    from SOCRadarIncidentsV4 import change_status_command

    mocker.patch.object(mock_client, "change_alarm_status", return_value={"is_success": True})

    result = change_status_command(mock_client, {"alarm_ids": "1,2", "status_reason": "RESOLVED"})
    assert "Status changed" in result.readable_output


def test_change_status_command_missing_params(mock_client):
    from SOCRadarIncidentsV4 import change_status_command

    with pytest.raises(ValueError, match="required"):
        change_status_command(mock_client, {"alarm_ids": "", "status_reason": ""})


# ---------------------------------------------------------------------------
# mark commands
# ---------------------------------------------------------------------------


def test_mark_false_positive_success(mock_client, mocker):
    from SOCRadarIncidentsV4 import mark_as_false_positive_command

    mocker.patch.object(mock_client, "change_alarm_status", return_value={"is_success": True})
    result = mark_as_false_positive_command(mock_client, {"alarm_id": "1"})
    assert "false positive" in result.readable_output


def test_mark_false_positive_missing_alarm_id(mock_client):
    from SOCRadarIncidentsV4 import mark_as_false_positive_command

    with pytest.raises(ValueError, match="alarm_id is required"):
        mark_as_false_positive_command(mock_client, {})


def test_mark_resolved_success(mock_client, mocker):
    from SOCRadarIncidentsV4 import mark_as_resolved_command

    mocker.patch.object(mock_client, "change_alarm_status", return_value={"is_success": True})
    result = mark_as_resolved_command(mock_client, {"alarm_id": "1"})
    assert "resolved" in result.readable_output


# ---------------------------------------------------------------------------
# add_comment / add_assignee / add_tag commands
# ---------------------------------------------------------------------------


def test_add_comment_success(mock_client, mocker):
    from SOCRadarIncidentsV4 import add_comment_command

    mocker.patch.object(mock_client, "add_alarm_comment", return_value={"is_success": True})
    result = add_comment_command(
        mock_client,
        {"alarm_id": "1", "user_email": "user@example.com", "comment": "test"},
    )
    assert "Comment added" in result.readable_output


def test_add_comment_missing_params(mock_client):
    from SOCRadarIncidentsV4 import add_comment_command

    with pytest.raises(ValueError):
        add_comment_command(mock_client, {"alarm_id": "1", "user_email": "", "comment": ""})


def test_add_assignee_success(mock_client, mocker):
    from SOCRadarIncidentsV4 import add_assignee_command

    mocker.patch.object(mock_client, "add_alarm_assignee", return_value={"is_success": True})
    result = add_assignee_command(mock_client, {"alarm_id": "1", "user_emails": "user@example.com"})
    assert "Assignee added" in result.readable_output


def test_add_assignee_missing_emails(mock_client):
    from SOCRadarIncidentsV4 import add_assignee_command

    with pytest.raises(ValueError, match="user_emails is required"):
        add_assignee_command(mock_client, {"alarm_id": "1", "user_emails": ""})


def test_add_tag_success(mock_client, mocker):
    from SOCRadarIncidentsV4 import add_tag_command

    mocker.patch.object(mock_client, "add_remove_tag", return_value={"is_success": True})
    result = add_tag_command(mock_client, {"alarm_id": "1", "tag": "critical"})
    assert "critical" in result.readable_output


def test_add_tag_missing_tag(mock_client):
    from SOCRadarIncidentsV4 import add_tag_command

    with pytest.raises(ValueError, match="tag is required"):
        add_tag_command(mock_client, {"alarm_id": "1", "tag": ""})


# ---------------------------------------------------------------------------
# test_fetch_command
# ---------------------------------------------------------------------------


def test_test_fetch_success(mock_client, mocker):
    from SOCRadarIncidentsV4 import test_fetch_command

    mocker.patch.object(
        mock_client,
        "search_incidents",
        return_value=_mock_response([_make_alarm()], total_records=1, total_pages=1),
    )

    result = test_fetch_command(mock_client, {"first_fetch": "3 days", "limit": "5"})
    assert "Found" in result.readable_output


def test_test_fetch_no_incidents(mock_client, mocker):
    from SOCRadarIncidentsV4 import test_fetch_command

    mocker.patch.object(mock_client, "search_incidents", return_value=_mock_response([]))

    result = test_fetch_command(mock_client, {"first_fetch": "3 days", "limit": "5"})
    assert "No incidents found" in result.readable_output


def test_client_search_incidents_error_response(mock_client, mocker, capfd):
    """Test that API error response raises DemistoException"""
    from CommonServerPython import DemistoException

    mocker.patch.object(
        mock_client,
        "_http_request",
        return_value={
            "is_success": False,
            "message": "Invalid request",
        },
    )

    with capfd.disabled(), pytest.raises(DemistoException, match="API Error"):
        mock_client.search_incidents(limit=1, page=1)


# ---------------------------------------------------------------------------
# format_value
# ---------------------------------------------------------------------------


def test_format_value_dict():
    from SOCRadarIncidentsV4 import format_value

    result = format_value({"key1": "val1", "key2": "val2"})
    assert "key1: val1" in result
    assert "key2: val2" in result


def test_format_value_nested_dict():
    from SOCRadarIncidentsV4 import format_value

    result = format_value({"parent": {"child": "value"}})
    assert "parent:" in result
    assert "  child: value" in result


def test_format_value_list():
    from SOCRadarIncidentsV4 import format_value

    result = format_value(["item1", "item2"])
    assert "- item1" in result
    assert "- item2" in result


def test_format_value_list_of_dicts():
    from SOCRadarIncidentsV4 import format_value

    result = format_value([{"k": "v"}])
    assert "-" in result[0]
    assert "k: v" in result[1]


def test_format_value_scalar():
    from SOCRadarIncidentsV4 import format_value

    result = format_value("simple_string")
    assert result == ["simple_string"]


def test_format_value_dict_with_list_value():
    from SOCRadarIncidentsV4 import format_value

    result = format_value({"items": ["a", "b"]})
    assert "items:" in result
    assert any("- a" in line for line in result)


# ---------------------------------------------------------------------------
# alarm_to_incident include_* parameters
# ---------------------------------------------------------------------------


def test_alarm_to_incident_include_mitigation():
    import json
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["alarm_type_details"]["alarm_default_mitigation_plan"] = "Do X and Y"

    incident = alarm_to_incident(alarm, include_mitigation=True)
    assert incident["CustomFields"]["socradarmitigation"] == "Do X and Y"
    raw = json.loads(incident["rawJSON"])
    assert raw.get("alarm_mitigation") == "Do X and Y"


def test_alarm_to_incident_include_response():
    import json
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["alarm_response"] = "Response steps here"

    incident = alarm_to_incident(alarm, include_response=True)
    assert incident["CustomFields"]["socradarresponse"] == "Response steps here"
    raw = json.loads(incident["rawJSON"])
    assert raw.get("alarm_response_plan") == "Response steps here"


def test_alarm_to_incident_include_detection_and_analysis():
    import json
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["alarm_type_details"]["alarm_detection_and_analysis"] = "Detection info"

    incident = alarm_to_incident(alarm, include_detection_and_analysis=True)
    assert incident["CustomFields"]["socradardetectionandanalysis"] == "Detection info"
    raw = json.loads(incident["rawJSON"])
    assert raw.get("alarm_detection_and_analysis") == "Detection info"


def test_alarm_to_incident_include_post_incident_analysis():
    import json
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["alarm_type_details"]["alarm_post_incident_analysis"] = "Post analysis"

    incident = alarm_to_incident(alarm, include_post_incident_analysis=True)
    assert incident["CustomFields"]["socradarpostincidentanalysis"] == "Post analysis"
    raw = json.loads(incident["rawJSON"])
    assert raw.get("alarm_post_incident_analysis") == "Post analysis"


def test_alarm_to_incident_include_related_assets():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["alarm_related_assets"] = [{"key": "domain", "value": ["example.com", "test.com"]}]

    incident = alarm_to_incident(alarm, include_related_assets=True)
    assert "domain: example.com || test.com" in incident["CustomFields"]["socradarrelatedassets"]


def test_alarm_to_incident_include_related_entities():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["alarm_related_entities"] = [{"key": "IP", "value": "1.2.3.4"}]

    incident = alarm_to_incident(alarm, include_related_entities=True)
    assert "IP: 1.2.3.4" in incident["CustomFields"]["socradarrelatedentities"]


def test_alarm_to_incident_include_company_id():
    import json
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    # Remove company_id from alarm to test configured_company_id fallback
    del alarm["company_id"]

    incident = alarm_to_incident(alarm, include_company_id=True, configured_company_id="789")
    assert incident["CustomFields"]["socradarcompanyid"] == "789"
    raw = json.loads(incident["rawJSON"])
    assert raw.get("company_id") == "789"


def test_alarm_to_incident_include_company_id_from_alarm():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm(company_id="456")

    incident = alarm_to_incident(alarm, include_company_id=True)
    assert incident["CustomFields"]["socradarcompanyid"] == "456"


def test_alarm_to_incident_show_content_false():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["content"] = {"key1": "value1", "key2": "value2"}

    incident = alarm_to_incident(alarm, show_content=False, include_incident_link=True)
    assert incident["CustomFields"]["socradarincidentcontent"] == ""


def test_alarm_to_incident_show_content_true():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["content"] = {"key1": "value1"}

    incident = alarm_to_incident(alarm, show_content=True, include_incident_link=True)
    assert "key1: value1" in incident["CustomFields"]["socradarincidentcontent"]


def test_alarm_to_incident_non_dict_alarm_type_details():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["alarm_type_details"] = "not-a-dict"

    incident = alarm_to_incident(alarm)
    assert "Unknown" in incident["name"]


def test_alarm_to_incident_non_dict_content():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["content"] = "not-a-dict"

    incident = alarm_to_incident(alarm, show_content=True, include_incident_link=True)
    assert incident["CustomFields"]["socradarincidentcontent"] == ""


def test_alarm_to_incident_non_list_tags():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["tags"] = "not-a-list"

    incident = alarm_to_incident(alarm)
    assert incident["CustomFields"]["socradartags"] == ""


def test_alarm_to_incident_non_list_related_entities():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["alarm_related_entities"] = "not-a-list"

    incident = alarm_to_incident(alarm)
    # Should not crash
    assert incident["name"] is not None


def test_alarm_to_incident_non_list_related_assets():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["alarm_related_assets"] = "not-a-list"

    incident = alarm_to_incident(alarm)
    assert incident["name"] is not None


def test_alarm_to_incident_no_date():
    from SOCRadarIncidentsV4 import alarm_to_incident

    alarm = _make_alarm()
    alarm["date"] = None

    incident = alarm_to_incident(alarm)
    assert incident["occurred"].endswith("Z")


# ---------------------------------------------------------------------------
# search_incidents - data as list path
# ---------------------------------------------------------------------------


def test_search_incidents_data_as_list(mock_client, mocker):
    """Test that when API returns data as a list (no total_records), it is handled."""
    mocker.patch.object(
        mock_client,
        "_http_request",
        return_value={
            "is_success": True,
            "data": [{"alarm_id": 1}, {"alarm_id": 2}],
        },
    )

    result = mock_client.search_incidents(limit=10, page=1, include_total_records=False)
    assert result["total_records"] == 2
    assert result["total_pages"] == 1
    assert len(result["data"]) == 2


def test_search_incidents_include_company_id(mock_client, mocker):
    """Test that include_company_id passes the param to the API."""
    mock_http = mocker.patch.object(
        mock_client,
        "_http_request",
        return_value={
            "is_success": True,
            "data": {"alarms": [], "total_pages": 1, "total_records": 0},
        },
    )

    mock_client.search_incidents(limit=10, page=1, include_company_id=True)

    call_kwargs = mock_http.call_args[1]
    assert call_kwargs["params"]["include_company_id"] == "true"


def test_search_incidents_no_include_company_id(mock_client, mocker):
    """Test that include_company_id=False does not add the param."""
    mock_http = mocker.patch.object(
        mock_client,
        "_http_request",
        return_value={
            "is_success": True,
            "data": {"alarms": [], "total_pages": 1, "total_records": 0},
        },
    )

    mock_client.search_incidents(limit=10, page=1, include_company_id=False)

    call_kwargs = mock_http.call_args[1]
    assert "include_company_id" not in call_kwargs["params"]


# ---------------------------------------------------------------------------
# change_alarm_status - update_related_finding_status and email
# ---------------------------------------------------------------------------


def test_change_alarm_status_with_update_related_and_email(mock_client, mocker):
    """Test change_alarm_status passes update_related_finding_status and email."""
    mock_http = mocker.patch.object(
        mock_client,
        "_http_request",
        return_value={"is_success": True},
    )

    mock_client.change_alarm_status(
        [1],
        "RESOLVED",
        comments="test",
        update_related_finding_status=True,
        email="user@example.com",
    )

    call_kwargs = mock_http.call_args[1]
    assert call_kwargs["json_data"]["update_related_finding_status"] is True
    assert call_kwargs["json_data"]["email"] == "user@example.com"


def test_change_alarm_status_with_company_id(mock_client, mocker):
    """Test change_alarm_status uses provided company_id."""
    mock_http = mocker.patch.object(
        mock_client,
        "_http_request",
        return_value={"is_success": True},
    )

    mock_client.change_alarm_status([1], "OPEN", company_id="999")

    call_kwargs = mock_http.call_args[1]
    assert "/company/999/" in call_kwargs["url_suffix"]


def test_change_alarm_status_no_company_id(mocker):
    """Test change_alarm_status raises when no company_id available."""
    from SOCRadarIncidentsV4 import Client

    client = Client(
        base_url="https://platform.socradar.com/api",
        api_key="test",
        company_id="",
        verify=False,
        proxy=False,
    )

    with pytest.raises(ValueError, match="company_id must be provided"):
        client.change_alarm_status([1], "OPEN")


def test_change_alarm_status_api_error(mock_client, mocker):
    """Test change_alarm_status raises on API error."""
    from CommonServerPython import DemistoException

    mocker.patch.object(
        mock_client,
        "_http_request",
        return_value={"is_success": False, "message": "Server error"},
    )

    with pytest.raises(DemistoException, match="API Error"):
        mock_client.change_alarm_status([1], "RESOLVED")


# ---------------------------------------------------------------------------
# change_status_command - update_related_finding_status validation
# ---------------------------------------------------------------------------


def test_change_status_command_requires_email_for_update_related(mock_client, mocker):
    """Test that email is required when update_related_finding_status is true."""
    from SOCRadarIncidentsV4 import change_status_command

    with pytest.raises(ValueError, match="email"):
        change_status_command(
            mock_client,
            {
                "alarm_ids": "1",
                "status_reason": "RESOLVED",
                "update_related_finding_status": "true",
            },
        )


def test_change_status_command_with_update_related_and_email(mock_client, mocker):
    """Test change_status_command passes update_related and email correctly."""
    from SOCRadarIncidentsV4 import change_status_command

    mocker.patch.object(mock_client, "change_alarm_status", return_value={"is_success": True})

    result = change_status_command(
        mock_client,
        {
            "alarm_ids": "1,2",
            "status_reason": "RESOLVED",
            "update_related_finding_status": "true",
            "email": "user@test.com",
        },
    )
    assert "Status changed" in result.readable_output


# ---------------------------------------------------------------------------
# fetch_incidents include parameters pass-through
# ---------------------------------------------------------------------------


def test_fetch_incidents_passes_include_params(mock_client, mocker):
    """Test that include_* params are passed to alarm_to_incident."""
    from SOCRadarIncidentsV4 import fetch_incidents

    alarm = _make_alarm()
    alarm["alarm_type_details"]["alarm_default_mitigation_plan"] = "Mitigation plan"
    alarm["alarm_response"] = "Response plan"

    mocker.patch.object(mock_client, "search_incidents", return_value=_mock_response([alarm]))

    next_run, incidents = fetch_incidents(
        client=mock_client,
        max_results=100,
        last_run={},
        first_fetch_time="3 days",
        include_mitigation=True,
        include_response=True,
        include_company_id=True,
        configured_company_id="999",
    )

    assert len(incidents) == 1
    cf = incidents[0]["CustomFields"]
    assert cf.get("socradarmitigation") == "Mitigation plan"
    assert cf.get("socradarresponse") == "Response plan"
    assert cf.get("socradarcompanyid") is not None


# ---------------------------------------------------------------------------
# parse_alarm_date - additional format
# ---------------------------------------------------------------------------


def test_parse_alarm_date_space_format():
    from SOCRadarIncidentsV4 import parse_alarm_date

    result = parse_alarm_date("2024-01-15 10:30:00")
    assert result is not None
    assert result.year == 2024
    assert result.month == 1
    assert result.day == 15


# ---------------------------------------------------------------------------
# test_module edge cases
# ---------------------------------------------------------------------------


def test_test_module_unexpected_exception(mock_client, mocker, capfd):
    from SOCRadarIncidentsV4 import test_module

    mocker.patch.object(mock_client, "search_incidents", side_effect=RuntimeError("unexpected"))
    with capfd.disabled():
        result = test_module(mock_client)
    assert "Unexpected error" in result


# ---------------------------------------------------------------------------

# ---------------------------------------------------------------------------
# Client methods - missing company_id
# ---------------------------------------------------------------------------


def test_add_alarm_comment_no_company_id(mocker):
    from SOCRadarIncidentsV4 import Client

    client = Client(
        base_url="https://test.com",
        api_key="k",
        company_id="",
        verify=False,
        proxy=False,
    )
    with pytest.raises(ValueError, match="company_id"):
        client.add_alarm_comment(1, "user@test.com", "comment")


def test_add_alarm_assignee_no_company_id(mocker):
    from SOCRadarIncidentsV4 import Client

    client = Client(
        base_url="https://test.com",
        api_key="k",
        company_id="",
        verify=False,
        proxy=False,
    )
    with pytest.raises(ValueError, match="company_id"):
        client.add_alarm_assignee(1, user_emails=["user@test.com"])


def test_add_remove_tag_no_company_id(mocker):
    from SOCRadarIncidentsV4 import Client

    client = Client(
        base_url="https://test.com",
        api_key="k",
        company_id="",
        verify=False,
        proxy=False,
    )
    with pytest.raises(ValueError, match="company_id"):
        client.add_remove_tag(1, "tag")