Unit 42 Feed

Unit 42 Feed integration provides threat intelligence from Palo Alto Networks Unit 42 research team.

Data Enrichment & Threat Intelligence · Unit 42 Threat Intelligence by Palo Alto Networks · Feed

Details

IDUnit 42 Feed
ProviderPalo Alto Networks
CategoryData Enrichment & Threat Intelligence
From Version6.10.0
Docker Imagedemisto/python3:3.12.13.10404775
Supported ModulesAgentix Cortex Cloud EDR XSIAM Cloud Runtime Security Cloud Posture Security

README

Unit 42 Feed integration provides threat intelligence from Palo Alto Networks Unit 42 research team.

Configure Unit 42 Feed in Cortex

Parameter Description Required
Fetch indicators Select this check box to fetch indicators (default selected). True
Feed Types Choose the requested indicator feeds. Indicators feed and Threat Objects (actors, malware, campaigns, techniques, etc.) feed (default is both). True
Indicator Types Comma-separated list of indicator types to fetch (File, IP, URL, Domain). If not specified, all indicator types are fetched. False
Maximum Indicators Per Fetch Maximum number of indicators to fetch per type. False
Source Reliability Reliability of the source providing the intelligence context. True
Tags Supports CSV values. False
Traffic Light Protocol Color (TLP). The Traffic Light Protocol (TLP) designation is to apply to indicators fetched from the feed. False
Indicator Reputation Indicators from this integration instance will be marked with this reputation True
Feed Expiration Policy The feed’s expiration policy. True
Indicator Expiration Interval The indicator’s expiration policy. False
Create relationships Create relationships with other indicators. False
Bypass exclusion list When selected, the exclusion list is ignored for indicators from this feed. This means that if an indicator from this feed is on the exclusion list, the indicator might still be added to the system. False
Use system proxy settings   False
Trust any certificate (not secure)   False

How the Maximum Indicators Per Fetch Parameter Works

The Maximum Indicators Per Fetch parameter controls the maximum number of indicators fetched per type during each fetch cycle. The integration enforces a total limit of 100,000 indicators across all types to ensure optimal performance.

Limit Calculation Algorithm

The limit per indicator type is calculated using the following logic:

  1. If the limit is not specified or is negative:
    • Default limit per type = 100,000 / total_number_of_types
  2. If the limit × total_number_of_types > 100,000:
    • Adjusted limit per type = 100,000 / total_number_of_types
  3. Otherwise:
    • Uses the specified limit per type.

Examples

Example 1: No Limit Specified with 4 Types

  • Configuration: Threat Objects + 3 indicator types (IP, Domain, URL)
  • Total number of types: 4
  • Calculation: 100,000 / 4 = 25,000 per type
  • Result: Fetches up to 25,000 of each type (100,000 total)

Example 2: Limit Exceeds Total

  • Configuration: Limit = 30,000, with 4 types selected
  • Calculation: 30,000 × 4 = 120,000 > 100,000 (exceeds total limit)
  • Adjusted: 100,000 / 4 = 25,000 per type
  • Result: Fetches up to 25,000 of each type (100,000 total)

Example 3: Limit within Total

  • Configuration: Limit = 20,000, with 4 types selected
  • Calculation: 20,000 × 4 = 80,000 ≤ 100,000 (within total limit)
  • Result: Fetches up to 20,000 of each type (80,000 total)

Example 4: Single Type

  • Configuration: Limit not specified, only IP indicators selected
  • Total types: 1
  • Calculation: 100,000 / 1 = 100,000 per type
  • Result: Fetches up to 100,000 IP indicators

Fetch Priority Order

When multiple types are configured, the integration fetches in the following priority order:

  1. Threat Objects (if enabled)
  2. IP indicators
  3. Domain indicators
  4. URL indicators
  5. File indicators

If any type returns fewer indicators than its allocated limit, the unused quota is added to the last enabled type in the priority order, allowing it to fetch up to the total limit of 100,000 indicators.

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

unit42-get-indicators


Gets indicators from the feed.

Base Command

unit42-get-indicators

Input

Argument Name Description Required
indicator_types Comma-separated list of indicator types to fetch (File, IP, URL, Domain). If not specified, all indicator types are fetched. Possible values are: File, IP, URL, Domain. Default is File,IP,URL,Domain. Optional
limit The maximum number of indicators to return. The default is 10. The maximum is 5000. Default is 10. Optional

Context Output

There is no context output for this command.

unit42-get-threat-objects


Gets threat objects from the feed.

Base Command

unit42-get-threat-objects

Input

Argument Name Description Required
limit The maximum number of threat objects to return. The default is 10. The maximum is 5000. Default is 10. Optional

Context Output

There is no context output for this command.

Troubleshooting

  • HTTP 403 Forbidden error when fetching indicators behind a corporate proxy or firewall.
    • This occurs when the proxy or firewall blocks outbound requests to the Unit 42 Feed API endpoint (prod-us.tas.crtx.paloaltonetworks.com), which is not listed in the standard Cortex XSOAR System Requirements documentation.
    • Resolution: Add prod-us.tas.crtx.paloaltonetworks.com (or the wildcard *.tas.crtx.paloaltonetworks.com) to your proxy or firewall allowlist. The integration requires outbound HTTPS (port 443) access to the following endpoints:
      • https://prod-us.tas.crtx.paloaltonetworks.com/api/v1/feeds/indicators
      • https://prod-us.tas.crtx.paloaltonetworks.com/api/v1/feeds/threat_objects

Configuration parameters

  • feed — Fetch indicators
  • feed_types — Feed Types
  • indicator_types — Indicator Types
  • limit — Maximum Indicators Per Fetch
  • feedFetchInterval — Feed Fetch Interval
  • feedReputation — Indicator Reputation
  • feedReliability — Source Reliability
  • feedTags — Tags
  • tlp_color — Traffic Light Protocol Color (TLP).
  • feedExpirationPolicy
  • feedExpirationInterval — Indicator Expiration Interval
  • create_relationships — Create relationships
  • feedIncremental — Incremental feed
  • feedBypassExclusionList — Bypass exclusion list
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)

Commands (2)

  • unit42-get-indicators

    Gets indicators from the feed.

  • unit42-get-threat-objects

    Gets threat objects from the feed.

import pytest
from datetime import datetime, timedelta
from Unit42Feed import (
    Client,
    create_publications,
    get_threat_object_score,
    create_location_indicators_and_relationships,
    build_threat_object_description,
    test_module as unit42_test_module,
    unit42_error_handler,
    main,
    sort_indicator_types_by_priority,
    fetch_indicator_type,
    fetch_threat_objects_with_limit,
    calculate_limit_per_type,
    INDICATOR_TYPE_MAPPING,
    VERDICT_TO_SCORE,
    VALID_REGIONS,
    DATE_FORMAT,
    API_LIMIT,
    INTEGRATION_NAME,
    RETRY_COUNT,
    STATUS_CODES_TO_RETRY,
    TOTAL_INDICATOR_LIMIT,
)
from CommonServerPython import *


def mock_demisto_params(mocker, create_relationships=True):
    """Helper function to mock demisto.params() with common parameters"""
    return mocker.patch(
        "Unit42Feed.demisto.params",
        return_value={"create_relationships": create_relationships, "feedReliability": DBotScoreReliability.A},
    )


@pytest.fixture
def client():
    """
    Given:
        - Client initialization parameters
    When:
        - Creating a Unit42Feed client
    Then:
        - Returns properly configured client instance
    """
    headers = {"Authorization": "Bearer test_token"}
    return Client(headers=headers, verify=False, proxy=False)


def test_client_initialization():
    """
    Given:
        - Client configuration parameters
    When:
        - Initializing Unit42Feed Client
    Then:
        - Sets correct base URL and headers
    """
    headers = {"Authorization": "Bearer test_token"}
    client = Client(headers=headers, verify=True, proxy=True)

    assert client._base_url == "https://prod-us.tas.crtx.paloaltonetworks.com"
    assert client._headers == headers
    assert client._verify is True


def test_client_get_indicators(client, mocker):
    """
    Given:
        - A Unit42Feed client
        - Mock API response with indicators data
    When:
        - Calling get_indicators with various parameters
    Then:
        - Makes correct API request with proper parameters
        - Returns response data
    """
    mock_response = {
        "data": [{"indicator_value": "1.2.3.4", "indicator_type": "ip", "verdict": "malicious"}],
        "metadata": {"next_page_token": "token123"},
    }

    mock_http_request = mocker.patch.object(client, "_http_request", return_value=mock_response)

    # Test with all parameters
    result = client.get_indicators(
        indicator_types=["ip", "domain"], limit=100, start_time="2023-01-01T00:00:00Z", next_page_token="page_token"
    )

    assert result == mock_response
    mock_http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/v1/feeds/indicators",
        error_handler=unit42_error_handler,
        retries=RETRY_COUNT,
        status_list_to_retry=STATUS_CODES_TO_RETRY,
        params={
            "indicator_types": ["ip", "domain"],
            "limit": 100,
            "start_time": "2023-01-01T00:00:00Z",
            "page_token": "page_token",
        },
    )


def test_client_get_indicators_file_type_mapping(client, mocker):
    """
    Given:
        - A Unit42Feed client
        - File indicator type in request
    When:
        - Calling get_indicators with file type
    Then:
        - Maps file to filehash_sha256 in API request
    """
    mock_response = {"data": []}
    mock_http_request = mocker.patch.object(client, "_http_request", return_value=mock_response)

    client.get_indicators(indicator_types=["file"])

    mock_http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/v1/feeds/indicators",
        error_handler=unit42_error_handler,
        retries=RETRY_COUNT,
        status_list_to_retry=STATUS_CODES_TO_RETRY,
        params={"indicator_types": ["filehash_sha256"], "limit": 5000},
    )


def test_client_get_threat_objects(client, mocker):
    """
    Given:
        - A Unit42Feed client
        - Mock API response with threat objects data
    When:
        - Calling get_threat_objects with parameters
    Then:
        - Makes correct API request
        - Returns response data
    """
    mock_response = {"data": [{"name": "APT29", "threat_object_class": "actor"}], "metadata": {"next_page_token": "token456"}}

    mock_http_request = mocker.patch.object(client, "_http_request", return_value=mock_response)

    result = client.get_threat_objects(limit=50, next_page_token="test_token")

    assert result == mock_response
    mock_http_request.assert_called_once_with(
        method="GET",
        url_suffix="/api/v1/feeds/threat_objects",
        error_handler=unit42_error_handler,
        retries=RETRY_COUNT,
        status_list_to_retry=STATUS_CODES_TO_RETRY,
        params={"limit": 50, "page_token": "test_token"},
    )


def test_create_publications():
    """
    Given:
        - Publications data from threat object
    When:
        - Calling create_publications function
    Then:
        - Returns properly formatted publications list
        - Uses default source when not provided
    """
    publications_data = [
        {
            "created_at": "2023-01-01T00:00:00Z",
            "title": "Test Report",
            "url": "https://example.com/report",
            "source": "Custom Source",
        },
        {
            "created_at": "2023-02-01T00:00:00Z",
            "title": "Another Report",
            "url": "https://example.com/report2",
            # Missing source - should use default
        },
    ]

    result = create_publications(publications_data)

    assert len(result) == 2
    assert result[0]["link"] == "https://example.com/report"
    assert result[0]["title"] == "Test Report"
    assert result[0]["timestamp"] == "2023-01-01T00:00:00Z"
    assert result[0]["source"] == "Custom Source"

    assert result[1]["source"] == INTEGRATION_NAME  # Default source


def test_get_threat_object_score():
    """
    Given:
        - Various threat object classes
    When:
        - Calling get_threat_object_score function
    Then:
        - Returns correct ThreatIntel score for each class
        - Returns NONE for unknown classes
    """
    # Test malware family
    assert get_threat_object_score("malware_family") == ThreatIntel.ObjectsScore.MALWARE

    # Test threat actor
    assert get_threat_object_score("actor") == ThreatIntel.ObjectsScore.THREAT_ACTOR
    assert get_threat_object_score("threat_actor") == ThreatIntel.ObjectsScore.THREAT_ACTOR

    # Test campaign
    assert get_threat_object_score("campaign") == ThreatIntel.ObjectsScore.CAMPAIGN

    # Test attack patterns
    assert get_threat_object_score("attack pattern") == ThreatIntel.ObjectsScore.ATTACK_PATTERN
    assert get_threat_object_score("technique") == ThreatIntel.ObjectsScore.ATTACK_PATTERN
    assert get_threat_object_score("malicious_behavior") == ThreatIntel.ObjectsScore.ATTACK_PATTERN
    assert get_threat_object_score("malicious behavior") == ThreatIntel.ObjectsScore.ATTACK_PATTERN

    # Test unknown class
    assert get_threat_object_score("unknown_class") == Common.DBotScore.NONE


def test_build_threat_object_description():
    """
    Given:
        - Threat object data with various description fields
    When:
        - Calling build_threat_object_description function
    Then:
        - Builds comprehensive description with all sections
        - Handles missing fields gracefully
    """
    threat_obj = {
        "description": "Base description\\nwith newlines",
        "battlecard_details": {
            "highlights": "Key highlights\\nfor this threat",
            "threat_actor_details": {
                "methods": "Attack methods\\nused by actor",
                "targets": "Target information\\nfor this actor",
            },
        },
    }

    result = build_threat_object_description(threat_obj)

    assert "Base description\nwith newlines" in result
    assert "Key highlights\nfor this threat" in result
    assert "Attack methods\nused by actor" in result
    assert "Target information\nfor this actor" in result
    assert result.count("##") == 3  # Three sections added


def test_build_threat_object_description_minimal():
    """
    Given:
        - Threat object with only basic description
    When:
        - Calling build_threat_object_description function
    Then:
        - Returns only the basic description
        - Handles missing battlecard details
    """
    threat_obj = {"description": "Simple description"}

    result = build_threat_object_description(threat_obj)

    assert result == "Simple description"


def test_test_module_success(client, mocker):
    """
    Given:
        - A Unit42Feed client
        - Mock API response that succeeds
    When:
        - Running test_module function
    Then:
        - Returns 'ok' indicating successful connection
    """
    mock_response = {"data": [{"indicator_value": "test"}]}
    # Mock the _http_request method to avoid actual API calls
    mocker.patch.object(client, "_http_request", return_value=mock_response)

    result = unit42_test_module(client)

    assert result == "ok"


def test_test_module_empty_result(client, mocker):
    """
    Given:
        - A Unit42Feed client
        - Mock API response with no data
    When:
        - Running test_module function
    Then:
        - Returns ok
    """
    mock_response = {"data": []}
    # Mock the _http_request method to avoid actual API calls
    mocker.patch.object(client, "_http_request", return_value=mock_response)

    result = unit42_test_module(client)

    assert result == "ok"


def test_test_module_exception(client, mocker):
    """
    Given:
        - A Unit42Feed client
        - Mock API that raises exception
    When:
        - Running test_module function
    Then:
        - Raises an exception (since test_module doesn't catch exceptions)
    """
    # Mock the _http_request method to raise an exception
    mocker.patch.object(client, "_http_request", side_effect=Exception("API Error"))

    res = unit42_test_module(client)
    assert "Error: API Error" in res


def test_create_location_indicators_and_relationships():
    """
    Given:
        - Threat object with affected regions data
        - Threat actor name
    When:
        - Calling create_location_indicators_and_relationships function
    Then:
        - Creates location indicators for valid regions
        - Creates proper relationships with threat actor
        - Skips invalid regions
    """
    threat_obj = {
        "battlecard_details": {
            "threat_actor_details": {
                "affected_regions": [
                    "North America",
                    "europe",  # lowercase - should be standardized
                    "Invalid Region",  # not in VALID_REGIONS
                    "middle east",  # lowercase - should be standardized
                ]
            }
        }
    }
    threat_actor_name = "APT29"

    result = create_location_indicators_and_relationships(threat_obj, threat_actor_name)

    # Should create indicators for valid regions only
    assert len(result) == 3  # North America, Europe, Middle East

    # Check first location indicator
    location_indicator = result[0]
    assert location_indicator["type"] == FeedIndicatorType.Location
    assert location_indicator["score"] == Common.DBotScore.NONE
    assert location_indicator["service"] == INTEGRATION_NAME
    assert len(location_indicator["relationships"]) == 1

    # Check relationship
    relationship = location_indicator["relationships"][0]
    assert relationship["name"] == EntityRelationship.Relationships.TARGETS
    assert relationship["entityA"] == threat_actor_name
    assert relationship["entityAType"] == ThreatIntel.ObjectsNames.THREAT_ACTOR
    assert relationship["entityBType"] == FeedIndicatorType.Location


def test_create_location_indicators_null_regions():
    """
    Given:
        - Threat object with null affected_regions
    When:
        - Calling create_location_indicators_and_relationships function
    Then:
        - Returns empty list
        - Handles null gracefully
    """
    threat_obj = {"battlecard_details": {"threat_actor_details": {"affected_regions": None}}}

    result = create_location_indicators_and_relationships(threat_obj, "APT29")

    assert result == []


def test_create_vulnerabilities_relationships():
    """
    Given:
        - Threat object with vulnerability associations
        - Threat actor name and class
    When:
        - Calling create_vulnerabilities_relationships function
    Then:
        - Creates relationships for each CVE
        - Uses correct relationship type (EXPLOITS)
    """
    from Unit42Feed import create_vulnerabilities_relationships

    threat_obj = {
        "battlecard_details": {
            "threat_actor_details": {
                "vulnerability_associations": [
                    {"cve": "cve-2023-1234"},
                    {"cve": "CVE-2023-5678"},
                    {"other_field": "no_cve"},  # Should be skipped
                ]
            }
        }
    }

    result = create_vulnerabilities_relationships(threat_obj, "APT29", "actor")

    assert len(result) == 2

    # Check first relationship
    relationship = result[0]
    assert relationship["name"] == EntityRelationship.Relationships.EXPLOITS
    assert relationship["entityA"] == "APT29"
    assert relationship["entityB"] == "CVE-2023-1234"  # Should be uppercase
    assert relationship["entityBType"] == FeedIndicatorType.CVE


def test_create_relationships_and_tags():
    """
    Given:
        - Indicator value, type, and threat object associations
    When:
        - Calling create_relationships_and_tags function with relationships enabled
    Then:
        - Creates relationships based on threat object classes
        - Extracts threat object names as tags
        - Uses correct relationship types for different threat classes
    """
    from Unit42Feed import create_relationships_and_tags
    import unittest.mock

    threat_object_associations = [
        {"name": "APT29", "threat_object_class": "actor"},
        {"name": "Cobalt Strike", "threat_object_class": "malware_family"},
        {"name": "Operation Ghost", "threat_object_class": "campaign"},
        {"name": "Spear Phishing", "threat_object_class": "malicious_behavior"},
        {"name": "CVE-2023-1234", "threat_object_class": "exploit"},
        {"name": "", "threat_object_class": "actor"},  # Empty name - should be skipped
    ]

    with unittest.mock.patch("Unit42Feed.demisto.params") as mock_params:
        mock_params.return_value = {"create_relationships": True, "feedReliability": DBotScoreReliability.A}

        with unittest.mock.patch("Unit42Feed.argToBoolean", return_value=True):
            relationships, tags = create_relationships_and_tags("1.2.3.4", "ip", threat_object_associations)

    assert len(relationships) == 5  # Should skip empty name
    assert len(tags) == 5  # All valid names should be tags

    # Check tags
    assert "APT29" in tags
    assert "Cobalt Strike" in tags
    assert "Operation Ghost" in tags

    # Check relationship types
    relationship_names = [rel["name"] for rel in relationships]
    assert EntityRelationship.Relationships.USED_BY in relationship_names  # actor
    assert EntityRelationship.Relationships.PART_OF in relationship_names  # campaign
    assert EntityRelationship.Relationships.INDICATOR_OF in relationship_names  # malicious_behavior
    assert EntityRelationship.Relationships.EXPLOITS in relationship_names  # exploit


def test_map_indicator_basic():
    """
    Given:
        - Basic indicator data from API
    When:
        - Calling map_indicator function
    Then:
        - Returns properly formatted XSOAR indicator
        - Maps verdict to correct DBotScore
        - Sets correct indicator type
    """
    from Unit42Feed import map_indicator

    indicator_data = {
        "indicator_value": "1.2.3.4",
        "indicator_type": "ip",
        "verdict": "malicious",
        "updated_at": "2023-12-31T23:59:59Z",
        "first_seen": "2023-01-01T00:00:00Z",
        "source": "Unit42",
        "threat_object_associations": [],
    }

    result = map_indicator(indicator_data, feed_tags=["test_tag"], tlp_color="RED")

    assert result["value"] == "1.2.3.4"
    assert result["type"] == FeedIndicatorType.IP
    assert result["score"] == Common.DBotScore.BAD  # malicious verdict
    assert result["service"] == INTEGRATION_NAME
    assert result["fields"]["updateddate"] == "2023-12-31T23:59:59Z"
    assert result["fields"]["creationdate"] == "2023-01-01T00:00:00Z"
    assert result["fields"]["reportedby"] == "Unit42"
    assert "test_tag" in result["fields"]["tags"]
    assert result["fields"]["trafficlightprotocol"] == "RED"
    assert result["rawJSON"] == indicator_data


def test_map_indicator_file_type():
    """
    Given:
        - File indicator data with hash details
    When:
        - Calling map_indicator function
    Then:
        - Maps file hashes to correct fields
        - Sets file-specific fields
    """
    from Unit42Feed import map_indicator

    indicator_data = {
        "indicator_value": "a" * 64,  # SHA256 hash
        "indicator_type": "filehash_sha256",
        "verdict": "suspicious",
        "indicator_details": {
            "file_hashes": {
                "md5": "b" * 32,
                "sha1": "c" * 40,
                "sha256": "a" * 64,
                "ssdeep": "test_ssdeep",
                "imphash": "test_imphash",
                "pehash": "test_pehash",
            },
            "file_type": "exe",
            "file_size": 1024,
        },
        "threat_object_associations": [],
    }

    result = map_indicator(indicator_data)

    assert result["type"] == FeedIndicatorType.File
    assert result["score"] == Common.DBotScore.SUSPICIOUS
    assert result["fields"]["md5"] == "b" * 32
    assert result["fields"]["sha1"] == "c" * 40
    assert result["fields"]["sha256"] == "a" * 64
    assert result["fields"]["ssdeep"] == "test_ssdeep"
    assert result["fields"]["imphash"] == "test_imphash"
    assert result["fields"]["pehash"] == "test_pehash"
    assert result["fields"]["filetype"] == "exe"
    assert result["fields"]["fileextension"] == "exe"
    assert result["fields"]["size"] == 1024


def test_map_indicator_with_relationships():
    """
    Given:
        - Indicator data with threat object associations
    When:
        - Calling map_indicator function
    Then:
        - Creates relationships and tags from threat objects
    """
    from Unit42Feed import map_indicator
    import unittest.mock

    indicator_data = {
        "indicator_value": "malicious.com",
        "indicator_type": "domain",
        "verdict": "benign",
        "threat_object_associations": [
            {"name": "APT29", "threat_object_class": "actor"},
            {"name": "Cobalt Strike", "threat_object_class": "malware_family"},
        ],
    }

    with unittest.mock.patch("Unit42Feed.create_relationships_and_tags") as mock_create_rel:
        mock_create_rel.return_value = (
            [{"name": "test_relationship"}],  # relationships
            ["APT29", "Cobalt Strike"],  # tags
        )

        result = map_indicator(indicator_data)

    assert result["type"] == FeedIndicatorType.Domain
    assert result["score"] == Common.DBotScore.GOOD  # benign verdict
    assert len(result["relationships"]) == 1
    assert "APT29" in result["fields"]["tags"]
    assert "Cobalt Strike" in result["fields"]["tags"]


def test_map_threat_object_basic(mocker):
    """
    Given:
        - Basic threat object data from API
    When:
        - Calling map_threat_object function
    Then:
        - Returns list with properly formatted threat object
        - Sets correct threat object type and score
    """
    from Unit42Feed import map_threat_object

    mock_demisto_params(mocker)

    threat_object = {
        "name": "APT29",
        "threat_object_class": "actor",
        "last_hit": "2023-12-31T23:59:59Z",
        "sources": ["Unit42", "External"],
        "aliases": ["Cozy Bear", "The Dukes"],
        "publications": [],
        "related_threat_objects": [],
        "battlecard_details": {
            "industries": ["Finance", "Healthcare"],
            "threat_actor_details": {"primary_motivation": "espionage", "origin": "russia"},
        },
    }

    result = map_threat_object(threat_object, feed_tags=["test_tag"], tlp_color="AMBER")

    assert len(result) == 1  # Should return list with one threat object
    threat_obj = result[0]

    assert threat_obj["value"] == "APT29"
    assert threat_obj["type"] == ThreatIntel.ObjectsNames.THREAT_ACTOR
    assert threat_obj["score"] == ThreatIntel.ObjectsScore.THREAT_ACTOR
    assert threat_obj["service"] == INTEGRATION_NAME
    assert threat_obj["fields"]["lastseenbysource"] == "2023-12-31T23:59:59Z"
    assert threat_obj["fields"]["reportedby"] == ["Unit42", "External"]
    assert "Cozy Bear" in threat_obj["fields"]["aliases"]
    assert "The Dukes" in threat_obj["fields"]["aliases"]
    assert "Finance" in threat_obj["fields"]["industrysectors"]
    assert "Healthcare" in threat_obj["fields"]["industrysectors"]
    assert threat_obj["fields"]["primarymotivation"] == "Espionage"
    assert threat_obj["fields"]["geocountry"] == "RUSSIA"
    assert "test_tag" in threat_obj["fields"]["tags"]
    assert threat_obj["fields"]["trafficlightprotocol"] == "AMBER"


def test_map_threat_object_with_relationships(mocker):
    """
    Given:
        - Threat object data with relationships enabled
    When:
        - Calling map_threat_object function
    Then:
        - Creates relationships and location indicators
        - Includes all relationship types
    """
    from Unit42Feed import map_threat_object

    mock_demisto_params(mocker)

    threat_object = {
        "name": "APT29",
        "threat_object_class": "actor",
        "related_threat_objects": [{"name": "Cobalt Strike", "threat_object_class": "malware_family"}],
        "battlecard_details": {
            "campaigns": ["Operation Ghost"],
            "attack_patterns": [{"name": "Spear Phishing", "mitreid": "T1566"}],
            "threat_actor_details": {
                "malware_associations": [{"name": "Zeus"}],
                "tools": [{"name": "PowerShell"}],
                "vulnerability_associations": [{"cve": "CVE-2023-1234"}],
                "affected_regions": ["North America"],
            },
        },
        "publications": [],
    }

    # Mock demisto.params to enable relationships
    mocker.patch("Unit42Feed.demisto.params", return_value={"create_relationships": True})
    mocker.patch("Unit42Feed.argToBoolean", return_value=True)

    result = map_threat_object(threat_object)

    # Should include the main threat object plus location indicators
    assert len(result) >= 1

    # Find the main threat object (not a location indicator)
    main_threat_obj = next((obj for obj in result if obj["value"] == "APT29"), None)
    assert main_threat_obj is not None
    assert main_threat_obj["value"] == "APT29"
    assert len(main_threat_obj["relationships"]) > 0  # Should have relationships


def test_verdict_to_score_mapping():
    """
    Given:
        - Various verdict values
    When:
        - Using VERDICT_TO_SCORE mapping
    Then:
        - Maps verdicts to correct DBotScore values
    """
    assert VERDICT_TO_SCORE["malicious"] == Common.DBotScore.BAD
    assert VERDICT_TO_SCORE["suspicious"] == Common.DBotScore.SUSPICIOUS
    assert VERDICT_TO_SCORE["benign"] == Common.DBotScore.GOOD
    assert VERDICT_TO_SCORE["unknown"] == Common.DBotScore.NONE


def test_indicator_type_mapping():
    """
    Given:
        - Various indicator types from API
    When:
        - Using INDICATOR_TYPE_MAPPING
    Then:
        - Maps API types to correct XSOAR types
    """
    assert INDICATOR_TYPE_MAPPING["ip"] == FeedIndicatorType.IP
    assert INDICATOR_TYPE_MAPPING["domain"] == FeedIndicatorType.Domain
    assert INDICATOR_TYPE_MAPPING["url"] == FeedIndicatorType.URL
    assert INDICATOR_TYPE_MAPPING["file"] == FeedIndicatorType.File
    assert INDICATOR_TYPE_MAPPING["filehash_sha256"] == FeedIndicatorType.File
    assert INDICATOR_TYPE_MAPPING["malware_family"] == ThreatIntel.ObjectsNames.MALWARE
    assert INDICATOR_TYPE_MAPPING["actor"] == ThreatIntel.ObjectsNames.THREAT_ACTOR
    assert INDICATOR_TYPE_MAPPING["campaign"] == ThreatIntel.ObjectsNames.CAMPAIGN
    assert INDICATOR_TYPE_MAPPING["malicious_tool"] == ThreatIntel.ObjectsNames.TOOL
    assert INDICATOR_TYPE_MAPPING["attack_pattern"] == ThreatIntel.ObjectsNames.ATTACK_PATTERN
    assert INDICATOR_TYPE_MAPPING["vulnerability"] == FeedIndicatorType.CVE
    assert INDICATOR_TYPE_MAPPING["grayware"] == ThreatIntel.ObjectsNames.MALWARE
    assert "generic" not in INDICATOR_TYPE_MAPPING


def test_valid_regions_mapping():
    """
    Given:
        - Various region names
    When:
        - Using VALID_REGIONS mapping
    Then:
        - Maps lowercase regions to standardized names
    """
    assert VALID_REGIONS["north america"] == "North America"
    assert VALID_REGIONS["europe"] == "Europe"
    assert VALID_REGIONS["middle east"] == "Middle East"
    assert VALID_REGIONS["africa"] == "Africa"


def test_parse_indicators():
    """
    Given:
        - List of indicator data from API
    When:
        - Calling parse_indicators function
    Then:
        - Returns list of mapped indicators
        - Handles empty and invalid data gracefully
    """
    from Unit42Feed import parse_indicators

    indicators_data = [
        {"indicator_value": "1.2.3.4", "indicator_type": "ip", "verdict": "malicious"},
        {"indicator_value": "example.com", "indicator_type": "domain", "verdict": "benign"},
    ]

    result = parse_indicators(indicators_data, feed_tags=["test_tag"], tlp_color="GREEN")

    assert len(result) == 2
    assert result[0]["value"] == "1.2.3.4"
    assert result[0]["type"] == FeedIndicatorType.IP
    assert result[1]["value"] == "example.com"
    assert result[1]["type"] == FeedIndicatorType.Domain

    # Test with empty data
    empty_result = parse_indicators([])
    assert empty_result == []

    # Test with None data
    none_result = parse_indicators(None)
    assert none_result == []


def test_parse_threat_objects(mocker):
    """
    Given:
        - List of threat object data from API
    When:
        - Calling parse_threat_objects function
    Then:
        - Returns list of mapped threat objects
        - Handles empty and invalid data gracefully
    """
    from Unit42Feed import parse_threat_objects

    mock_demisto_params(mocker)

    threat_objects_data = [
        {"name": "APT29", "threat_object_class": "actor", "publications": []},
        {"name": "Cobalt Strike", "threat_object_class": "malware_family", "publications": []},
    ]

    result = parse_threat_objects(threat_objects_data, feed_tags=["test_tag"], tlp_color="AMBER")

    assert len(result) >= 2  # Could be more due to location indicators

    # Find the main threat objects (not location indicators)
    main_objects = [
        obj for obj in result if obj["type"] in [ThreatIntel.ObjectsNames.THREAT_ACTOR, ThreatIntel.ObjectsNames.MALWARE]
    ]

    assert len(main_objects) == 2
    assert any(obj["value"] == "APT29" for obj in main_objects)
    assert any(obj["value"] == "Cobalt Strike" for obj in main_objects)

    # Test with empty data
    empty_result = parse_threat_objects([])
    assert empty_result == []

    # Test with None data
    none_result = parse_threat_objects(None)
    assert none_result == []


def test_fetch_indicators_basic(client, mocker):
    """
    Given:
        - Unit42Feed client and parameters
        - Mock API responses for indicators
    When:
        - Calling fetch_indicators function
    Then:
        - Fetches indicators from API
        - Pushes both indicators and threat objects to the server immediately
        - Returns the total count fetched
    """
    from Unit42Feed import fetch_indicators

    # Mock API responses
    mock_indicators_response = {
        "data": [{"indicator_value": "1.2.3.4", "indicator_type": "ip", "verdict": "malicious"}],
        "metadata": {"next_page_token": None},
    }

    mock_threat_objects_response = {
        "data": [{"name": "APT29", "threat_object_class": "actor", "publications": []}],
        "metadata": {"next_page_token": None},
    }

    mocker.patch.object(client, "get_indicators", return_value=mock_indicators_response)
    mocker.patch.object(client, "get_threat_objects", return_value=mock_threat_objects_response)

    mock_create_indicators = mocker.patch("Unit42Feed.demisto.createIndicators")

    # Mock demisto functions
    mocker.patch("Unit42Feed.demisto.getLastRun", return_value={})
    mock_demisto_params(mocker)

    params = {
        "feed_types": ["Indicators", "Threat Objects"],
        "indicator_types": ["ip", "domain"],
        "feed_tags": ["test_tag"],
        "trafficlightprotocol": "RED",
    }

    current_time = datetime.now()
    result = fetch_indicators(client, params, current_time)

    assert result >= 2  # At least one indicator and one threat object

    # Check that both indicators and threat objects were pushed to the server
    pushed_items = [item for call in mock_create_indicators.call_args_list for item in call[0][0]]
    pushed_types = [item["type"] for item in pushed_items]
    assert FeedIndicatorType.IP in pushed_types
    assert ThreatIntel.ObjectsNames.THREAT_ACTOR in pushed_types


def test_fetch_indicators_pagination(client, mocker):
    """
    Given:
        - Unit42Feed client with paginated API responses
    When:
        - Calling fetch_indicators function
    Then:
        - Handles pagination correctly
        - Fetches multiple pages until limit or no more pages
        - Pushes indicators from both pages to the server
    """
    from Unit42Feed import fetch_indicators

    # Mock paginated responses
    first_response = {
        "data": [{"indicator_value": "1.2.3.4", "indicator_type": "ip", "verdict": "malicious"}],
        "metadata": {"next_page_token": "page2"},
    }

    second_response = {
        "data": [{"indicator_value": "5.6.7.8", "indicator_type": "ip", "verdict": "benign"}],
        "metadata": {"next_page_token": None},
    }

    mock_get_indicators = mocker.patch.object(client, "get_indicators")
    mock_get_indicators.side_effect = [first_response, second_response]

    mocker.patch.object(client, "get_threat_objects", return_value={"data": [], "metadata": {}})
    mocker.patch("Unit42Feed.demisto.getLastRun", return_value={})
    mock_create_indicators = mocker.patch("Unit42Feed.demisto.createIndicators")

    params = {"feed_types": ["Indicators"], "indicator_types": ["ip"], "feed_tags": [], "tlp_color": None}

    current_time = datetime.now()
    result = fetch_indicators(client, params, current_time)

    assert result == 2

    # Should have indicators from both pages, pushed to the server
    pushed_items = [item for call in mock_create_indicators.call_args_list for item in call[0][0]]
    ip_indicators = [item for item in pushed_items if item["type"] == FeedIndicatorType.IP]
    assert len(ip_indicators) == 2
    assert any(item["value"] == "1.2.3.4" for item in ip_indicators)
    assert any(item["value"] == "5.6.7.8" for item in ip_indicators)


def test_get_indicators_command(client, mocker):
    """
    Given:
        - Unit42Feed client and command arguments
    When:
        - Calling get_indicators_command function
    Then:
        - Returns CommandResults with indicators
        - Creates proper human readable output
    """
    from Unit42Feed import get_indicators_command

    mock_response = {
        "data": [{"indicator_value": "1.2.3.4", "indicator_type": "ip", "verdict": "malicious"}],
        "metadata": {"next_page_token": "token123"},
    }

    mocker.patch.object(client, "get_indicators", return_value=mock_response)

    args = {"limit": "5", "indicator_types": ["ip", "domain"], "next_page_token": "test_token"}

    result = get_indicators_command(client, args, feed_tags=["test_tag"], tlp_color="AMBER")

    assert isinstance(result, CommandResults)
    assert result.outputs_prefix == "Unit42.Indicator"
    assert result.outputs_key_field == "value"
    assert len(result.outputs) == 1
    assert result.outputs[0]["value"] == "1.2.3.4"
    assert "Unit 42 Indicators:" in result.readable_output
    assert result.raw_response == mock_response


def test_get_threat_objects_command(client, mocker):
    """
    Given:
        - Unit42Feed client and command arguments
    When:
        - Calling get_threat_objects_command function
    Then:
        - Returns CommandResults with threat objects
        - Creates proper human readable output
    """
    from Unit42Feed import get_threat_objects_command

    mock_response = {
        "data": [{"name": "APT29", "threat_object_class": "actor", "publications": []}],
        "metadata": {"next_page_token": "token456"},
    }

    mocker.patch.object(client, "get_threat_objects", return_value=mock_response)
    mock_demisto_params(mocker)

    args = {"limit": "10", "next_page_token": "test_token"}

    result = get_threat_objects_command(client, args, feed_tags=["test_tag"], tlp_color="GREEN")

    assert isinstance(result, CommandResults)
    assert result.outputs_prefix == "Unit42.ThreatObject"
    assert result.outputs_key_field == "value"
    assert len(result.outputs) >= 1  # Could include location indicators
    assert "Unit 42 Threat Objects:" in result.readable_output
    assert result.raw_response == mock_response


def test_date_format_constant():
    """
    Given:
        - DATE_FORMAT constant
    When:
        - Using the constant for date formatting
    Then:
        - Formats dates correctly
    """
    test_date = datetime(2023, 12, 31, 23, 59, 59)
    formatted_date = test_date.strftime(DATE_FORMAT)

    assert formatted_date == "2023-12-31T23:59:59Z"


def test_api_limit_constant():
    """
    Given:
        - API_LIMIT constant
    When:
        - Using the constant for API requests
    Then:
        - Has expected value for pagination
    """
    assert API_LIMIT == 5000
    assert isinstance(API_LIMIT, int)
    assert API_LIMIT > 0


def test_fetch_indicators_with_last_run(client, mocker):
    """
    Given:
        - Unit42Feed client with existing last run data
    When:
        - Calling fetch_indicators function
    Then:
        - Uses last run time for start_time parameter
        - Falls back to default if no last run
    """
    from Unit42Feed import fetch_indicators

    mock_response = {"data": [], "metadata": {}}
    mock_get_indicators = mocker.patch.object(client, "get_indicators", return_value=mock_response)
    mocker.patch.object(client, "get_threat_objects", return_value=mock_response)

    # Test with existing last run
    mocker.patch("Unit42Feed.demisto.getLastRun", return_value={"last_successful_run": "2023-06-01T12:00:00Z"})

    params = {"feed_types": ["Indicators"], "indicator_types": ["ip"], "feed_tags": [], "tlp_color": None}

    current_time = datetime(2023, 6, 2, 12, 0, 0)
    fetch_indicators(client, params, current_time)

    # Should use the last run time
    mock_get_indicators.assert_called_once()
    call_args = mock_get_indicators.call_args[1]
    assert call_args["start_time"] == "2023-06-01T12:00:00Z"


def test_fetch_indicators_default_start_time(client, mocker):
    """
    Given:
        - Unit42Feed client with no last run data
    When:
        - Calling fetch_indicators function
    Then:
        - Uses default start time (24 hours ago)
    """
    from Unit42Feed import fetch_indicators

    mock_response = {"data": [], "metadata": {}}
    mock_get_indicators = mocker.patch.object(client, "get_indicators", return_value=mock_response)
    mocker.patch.object(client, "get_threat_objects", return_value=mock_response)

    # Test with no last run
    mocker.patch("Unit42Feed.demisto.getLastRun", return_value={})

    params = {"feed_types": ["Indicators"], "indicator_types": ["ip"], "feed_tags": [], "tlp_color": None}

    current_time = datetime(2023, 6, 2, 12, 0, 0)
    fetch_indicators(client, params, current_time)

    # Should use default time (24 hours ago)
    mock_get_indicators.assert_called_once()
    call_args = mock_get_indicators.call_args[1]
    expected_default = (current_time - timedelta(hours=24)).strftime(DATE_FORMAT)
    assert call_args["start_time"] == expected_default


def test_main_function_test_module(mocker):
    """
    Given:
        - Main function with test-module command
    When:
        - Calling main function
    Then:
        - Executes test_module and returns results
    """
    # Mock demisto functions
    mock_params = {"insecure": False, "proxy": False, "feedFetchInterval": "720"}
    mocker.patch("Unit42Feed.demisto.params", return_value=mock_params)
    mocker.patch("Unit42Feed.demisto.command", return_value="test-module")
    mocker.patch("Unit42Feed.demisto.getLicenseID", return_value="test_license")
    mock_return_results = mocker.patch("Unit42Feed.return_results")

    # Mock Client and test_module
    mock_client = mocker.Mock()
    mocker.patch("Unit42Feed.Client", return_value=mock_client)
    mocker.patch("Unit42Feed.test_module", return_value="ok")

    main()

    mock_return_results.assert_called_once_with("ok")


def test_main_function_fetch_indicators(mocker):
    """
    Given:
        - Main function with fetch-indicators command
    When:
        - Calling main function
    Then:
        - Executes fetch_indicators, which pushes indicators to the server internally
        - main() itself no longer accumulates or re-batches indicators
    """
    # Mock demisto functions
    mock_params = {"insecure": False, "proxy": False, "feedFetchInterval": "720"}
    mocker.patch("Unit42Feed.demisto.params", return_value=mock_params)
    mocker.patch("Unit42Feed.demisto.command", return_value="fetch-indicators")
    mocker.patch("Unit42Feed.demisto.getLicenseID", return_value="test_license")
    mock_set_last_run = mocker.patch("Unit42Feed.demisto.setLastRun")
    mock_info = mocker.patch("Unit42Feed.demisto.info")

    # Mock Client and fetch_indicators (fetch_indicators now returns a count and pushes internally)
    mock_client = mocker.Mock()
    mocker.patch("Unit42Feed.Client", return_value=mock_client)
    mock_fetch_indicators = mocker.patch("Unit42Feed.fetch_indicators", return_value=1)
    mocker.patch("Unit42Feed.datetime")

    main()

    mock_fetch_indicators.assert_called_once()
    mock_set_last_run.assert_called_once()
    mock_info.assert_called_once()


def test_main_function_get_indicators_command(mocker):
    """
    Given:
        - Main function with unit42-get-indicators command
    When:
        - Calling main function
    Then:
        - Executes get_indicators_command and returns results
    """
    # Mock demisto functions
    mock_params = {"insecure": False, "proxy": False, "feedFetchInterval": "720"}
    mocker.patch("Unit42Feed.demisto.params", return_value=mock_params)
    mocker.patch("Unit42Feed.demisto.command", return_value="unit42-get-indicators")
    mocker.patch("Unit42Feed.demisto.args", return_value={"limit": "10"})
    mocker.patch("Unit42Feed.demisto.getLicenseID", return_value="test_license")
    mock_return_results = mocker.patch("Unit42Feed.return_results")

    # Mock Client and command
    mock_client = mocker.Mock()
    mocker.patch("Unit42Feed.Client", return_value=mock_client)
    mock_command_results = mocker.Mock()
    mocker.patch("Unit42Feed.get_indicators_command", return_value=mock_command_results)

    main()

    mock_return_results.assert_called_once_with(mock_command_results)


def test_main_function_get_threat_objects_command(mocker):
    """
    Given:
        - Main function with unit42-get-threat-objects command
    When:
        - Calling main function
    Then:
        - Executes get_threat_objects_command and returns results
    """
    # Mock demisto functions
    mock_params = {"insecure": False, "proxy": False, "feedFetchInterval": "720"}
    mocker.patch("Unit42Feed.demisto.params", return_value=mock_params)
    mocker.patch("Unit42Feed.demisto.command", return_value="unit42-get-threat-objects")
    mocker.patch("Unit42Feed.demisto.args", return_value={"limit": "5"})
    mocker.patch("Unit42Feed.demisto.getLicenseID", return_value="test_license")
    mock_return_results = mocker.patch("Unit42Feed.return_results")

    # Mock Client and command
    mock_client = mocker.Mock()
    mocker.patch("Unit42Feed.Client", return_value=mock_client)
    mock_command_results = mocker.Mock()
    mocker.patch("Unit42Feed.get_threat_objects_command", return_value=mock_command_results)

    main()

    mock_return_results.assert_called_once_with(mock_command_results)


def test_main_function_invalid_fetch_interval(mocker):
    """
    Given:
        - Main function with invalid feedFetchInterval (less than 720 minutes)
    When:
        - Calling main function
    Then:
        - Returns error about minimum fetch interval
    """
    # Mock demisto functions
    mock_params = {
        "insecure": False,
        "proxy": False,
        "feedFetchInterval": "600",  # Less than 720 minutes
    }
    mocker.patch("Unit42Feed.demisto.params", return_value=mock_params)
    mock_return_error = mocker.patch("Unit42Feed.return_error")

    main()

    mock_return_error.assert_called_once_with("Feed Fetch Interval parameter must be set to at least 12 hours.")


def test_main_function_exception_handling(mocker):
    """
    Given:
        - Main function that encounters an exception
    When:
        - Calling main function
    Then:
        - Handles exception and returns error message
    """
    # Mock demisto functions
    mock_params = {"insecure": False, "proxy": False, "feedFetchInterval": "720"}
    mocker.patch("Unit42Feed.demisto.params", return_value=mock_params)
    mocker.patch("Unit42Feed.demisto.command", return_value="test-module")
    mocker.patch("Unit42Feed.demisto.getLicenseID", return_value="test_license")
    mock_return_error = mocker.patch("Unit42Feed.return_error")

    # Mock Client to raise exception
    mocker.patch("Unit42Feed.Client", side_effect=Exception("Test error"))

    main()

    mock_return_error.assert_called_once()
    error_call = mock_return_error.call_args[0][0]
    assert "Failed to execute test-module command" in error_call
    assert "Test error" in error_call


def test_unit42_error_handler_with_request_id(mocker):
    """
    Given:
        - A mock requests.Response object with a status code, URL, and an X-Request-ID header.
    When:
        - Calling unit42_error_handler.
    Then:
        - demisto.return_error is called with a formatted error message including the X-Request-ID.
    """
    mock_response = mocker.Mock()
    mock_response.status_code = 500
    mock_response.url = "https://example.com/api"
    mock_response.text = "Internal Server Error"
    mock_response.headers = {"X-Request-ID": "test-request-id-123"}

    mocker.patch.object(demisto, "debug")
    mock_return_error = mocker.patch("Unit42Feed.return_error")

    unit42_error_handler(mock_response)

    expected_error_msg = (
        "Error in API request [Status: 500]\n" "[X-Request-ID: test-request-id-123]\n" "Response text - Internal Server Error"
    )
    mock_return_error.assert_called_once_with(expected_error_msg)
    demisto.debug.assert_called_once_with(
        f"{INTEGRATION_NAME} API Error - X-Request-ID: test-request-id-123, Status: 500, URL: https://example.com/api"
    )


def test_sort_indicator_types_by_priority():
    """
    Given:
        - Unsorted list of indicator types
    When:
        - Calling sort_indicator_types_by_priority function
    Then:
        - Returns sorted list with IPs first, Files last (bottom-to-top priority)
    """
    # Test with unsorted types
    unsorted_types = ["File", "IP", "Domain", "URL"]
    result = sort_indicator_types_by_priority(unsorted_types)

    assert result == ["IP", "Domain", "URL", "File"]

    # Test with partial list
    partial_types = ["File", "IP"]
    result = sort_indicator_types_by_priority(partial_types)

    assert result == ["IP", "File"]

    # Test with single type
    single_type = ["Domain"]
    result = sort_indicator_types_by_priority(single_type)

    assert result == ["Domain"]

    # Test with empty list
    empty_list = []
    result = sort_indicator_types_by_priority(empty_list)

    assert result == []


def test_fetch_indicator_type_with_limit(client, mocker):
    """
    Given:
        - Client and limit parameter
    When:
        - Calling fetch_indicator_type with limit smaller than API response
    Then:
        - Returns a count of only up to the limit
        - Pushes only up to the limit to the server
        - Makes correct API calls with page_limit
    """
    # Mock responses - first page has 100 items, second page has 50
    first_response = {
        "data": [{"indicator_value": f"1.2.3.{i}", "indicator_type": "ip", "verdict": "malicious"} for i in range(100)],
        "metadata": {"next_page_token": "page2"},
    }

    second_response = {
        "data": [{"indicator_value": f"5.6.7.{i}", "indicator_type": "ip", "verdict": "malicious"} for i in range(50)],
        "metadata": {"next_page_token": None},
    }

    mock_get_indicators = mocker.patch.object(client, "get_indicators")
    mock_get_indicators.side_effect = [first_response, second_response]
    mock_create_indicators = mocker.patch("Unit42Feed.demisto.createIndicators")

    # Fetch with limit of 120 (should get 100 from first page, 20 from second)
    result = fetch_indicator_type(
        client=client, indicator_type="IP", limit=120, start_time="2023-01-01T00:00:00Z", feed_tags=[], tlp_color=None
    )

    assert result == 120
    assert mock_get_indicators.call_count == 2

    pushed_items = [item for call in mock_create_indicators.call_args_list for item in call[0][0]]
    assert len(pushed_items) == 120

    # Check first call had limit of 100 (min of API_LIMIT and remaining)
    first_call_args = mock_get_indicators.call_args_list[0][1]
    assert first_call_args["limit"] <= API_LIMIT

    # Check second call had limit of 20 (remaining after first page)
    second_call_args = mock_get_indicators.call_args_list[1][1]
    assert second_call_args["limit"] == 20


def test_fetch_indicator_type_stops_at_limit(client, mocker):
    """
    Given:
        - Client with API returning more data than limit
    When:
        - Calling fetch_indicator_type with small limit
    Then:
        - Stops fetching when limit is reached
        - Returns exactly limit number of indicators
    """
    # Mock response with 100 items
    mock_response = {
        "data": [{"indicator_value": f"1.2.3.{i}", "indicator_type": "ip", "verdict": "malicious"} for i in range(100)],
        "metadata": {"next_page_token": "page2"},
    }

    mock_get_indicators = mocker.patch.object(client, "get_indicators", return_value=mock_response)
    mocker.patch("Unit42Feed.demisto.createIndicators")

    # Fetch with limit of 50
    result = fetch_indicator_type(
        client=client, indicator_type="IP", limit=50, start_time="2023-01-01T00:00:00Z", feed_tags=[], tlp_color=None
    )

    assert result == 50
    assert mock_get_indicators.call_count == 1  # Should only make one call


def test_fetch_indicator_type_no_data(client, mocker):
    """
    Given:
        - Client with API returning no data
    When:
        - Calling fetch_indicator_type
    Then:
        - Returns a count of zero
        - Handles gracefully
    """
    mock_response = {"data": [], "metadata": {}}
    mocker.patch.object(client, "get_indicators", return_value=mock_response)
    mock_create_indicators = mocker.patch("Unit42Feed.demisto.createIndicators")

    result = fetch_indicator_type(
        client=client, indicator_type="IP", limit=100, start_time="2023-01-01T00:00:00Z", feed_tags=[], tlp_color=None
    )

    assert result == 0
    mock_create_indicators.assert_not_called()


def test_fetch_threat_objects_with_limit(client, mocker):
    """
    Given:
        - Client and limit parameter
    When:
        - Calling fetch_threat_objects_with_limit
    Then:
        - Returns count of threat objects up to the limit
        - Pushes threat objects to the server
        - Handles pagination correctly
    """
    mock_demisto_params(mocker)

    # Mock responses
    first_response = {
        "data": [{"name": f"APT{i}", "threat_object_class": "actor", "publications": []} for i in range(100)],
        "metadata": {"next_page_token": "page2"},
    }

    second_response = {
        "data": [{"name": f"Malware{i}", "threat_object_class": "malware_family", "publications": []} for i in range(50)],
        "metadata": {"next_page_token": None},
    }

    mock_get_threat_objects = mocker.patch.object(client, "get_threat_objects")
    mock_get_threat_objects.side_effect = [first_response, second_response]
    mock_create_indicators = mocker.patch("Unit42Feed.demisto.createIndicators")

    # Fetch with limit of 120
    result = fetch_threat_objects_with_limit(client=client, limit=120, feed_tags=[], tlp_color=None)

    assert result == 120
    assert mock_get_threat_objects.call_count == 2

    pushed_items = [item for call in mock_create_indicators.call_args_list for item in call[0][0]]
    assert len(pushed_items) == 120


def test_fetch_indicators_limit_validation(client, mocker):
    """
    Given:
        - Client with various limit values
    When:
        - Calling fetch_indicators with different limits
    Then:
        - Validates and caps limit at TOTAL_INDICATOR_LIMIT
        - Uses DEFAULT_LIMIT when limit is invalid
    """
    from Unit42Feed import fetch_indicators

    mock_demisto_params(mocker)
    mock_response = {"data": [], "metadata": {}}
    mocker.patch.object(client, "get_indicators", return_value=mock_response)
    mocker.patch.object(client, "get_threat_objects", return_value=mock_response)
    mocker.patch("Unit42Feed.demisto.getLastRun", return_value={})

    current_time = datetime.now()

    # Test with limit exceeding maximum
    params_high = {
        "limit": "150000",  # Exceeds TOTAL_INDICATOR_LIMIT
        "feed_types": ["Indicators"],
        "indicator_types": ["IP"],
        "feedTags": [],
        "tlp_color": None,
    }

    fetch_indicators(client, params_high, current_time)
    # Should cap at TOTAL_INDICATOR_LIMIT (100K)
    assert True  # Function should complete without error

    # Test with zero limit
    params_zero = {
        "limit": "0",
        "feed_types": ["Indicators"],
        "indicator_types": ["IP"],
        "feedTags": [],
        "tlp_color": None,
    }

    fetch_indicators(client, params_zero, current_time)
    # Should use DEFAULT_LIMIT
    assert True  # Function should complete without error


def test_fetch_indicators_priority_order(client, mocker):
    """
    Given:
        - Client with all indicator types configured
        - Small limit to test priority
    When:
        - Calling fetch_indicators
    Then:
        - Fetches in correct priority order: Threat Objects → IP → Domain → URL → File
        - Stops when limit is reached
    """
    from Unit42Feed import fetch_indicators

    mock_demisto_params(mocker)

    # Track the order of API calls
    call_order = []

    def track_get_indicators(*args, **kwargs):
        indicator_types = kwargs.get("indicator_types", [])
        if indicator_types:
            call_order.append(indicator_types[0])
        return {
            "data": [{"indicator_value": "test", "indicator_type": indicator_types[0].lower(), "verdict": "malicious"}],
            "metadata": {"next_page_token": None},
        }

    def track_get_threat_objects(*args, **kwargs):
        call_order.append("ThreatObjects")
        return {
            "data": [{"name": "APT29", "threat_object_class": "actor", "publications": []}],
            "metadata": {"next_page_token": None},
        }

    mocker.patch.object(client, "get_indicators", side_effect=track_get_indicators)
    mocker.patch.object(client, "get_threat_objects", side_effect=track_get_threat_objects)
    mocker.patch("Unit42Feed.demisto.getLastRun", return_value={})

    params = {
        "limit": "10",
        "feed_types": ["Indicators", "Threat Objects"],
        "indicator_types": ["File", "URL", "Domain", "IP"],  # Unsorted
        "feedTags": [],
        "tlp_color": None,
    }

    current_time = datetime.now()
    fetch_indicators(client, params, current_time)

    # Verify priority order: Threat Objects first, then IP, Domain, URL, File
    assert call_order[0] == "ThreatObjects"
    assert call_order[1] == "IP"
    assert call_order[2] == "Domain"
    assert call_order[3] == "URL"
    assert call_order[4] == "File"


def test_fetch_indicator_type_pagination(client, mocker):
    """
    Given:
        - Client with paginated API responses
        - Limit requiring multiple pages
    When:
        - Calling fetch_indicator_type
    Then:
        - Fetches multiple pages until limit is reached
        - Calculates correct page_limit for each request
    """
    # Create responses for pagination
    responses = []
    for page in range(3):
        responses.append(
            {
                "data": [
                    {"indicator_value": f"1.2.{page}.{i}", "indicator_type": "ip", "verdict": "malicious"} for i in range(100)
                ],
                "metadata": {"next_page_token": f"page{page+2}" if page < 2 else None},
            }
        )

    mock_get_indicators = mocker.patch.object(client, "get_indicators")
    mock_get_indicators.side_effect = responses
    mocker.patch("Unit42Feed.demisto.createIndicators")

    # Fetch with limit of 250 (should get 100 + 100 + 50)
    result = fetch_indicator_type(
        client=client, indicator_type="IP", limit=250, start_time="2023-01-01T00:00:00Z", feed_tags=[], tlp_color=None
    )

    assert result == 250
    assert mock_get_indicators.call_count == 3

    # Verify the third call requested only 50 (remaining)
    third_call_args = mock_get_indicators.call_args_list[2][1]
    assert third_call_args["limit"] == 50


def test_fetch_threat_objects_with_limit_stops_early(client, mocker):
    """
    Given:
        - Client with API returning fewer results than limit
    When:
        - Calling fetch_threat_objects_with_limit
    Then:
        - Stops when no more data available
        - Returns all available data (less than limit)
    """
    mock_demisto_params(mocker)

    mock_response = {
        "data": [{"name": f"APT{i}", "threat_object_class": "actor", "publications": []} for i in range(25)],
        "metadata": {"next_page_token": None},
    }

    mock_get_threat_objects = mocker.patch.object(client, "get_threat_objects", return_value=mock_response)
    mocker.patch("Unit42Feed.demisto.createIndicators")

    # Request limit of 100, but only 25 available
    result = fetch_threat_objects_with_limit(client=client, limit=100, feed_tags=[], tlp_color=None)

    assert result == 25
    assert mock_get_threat_objects.call_count == 1


def test_calculate_limit_per_type_with_none():
    """
    Given:
        - limit is None
        - total_indicator_types is 4
    When:
        - Calling calculate_limit_per_type
    Then:
        - Returns default limit per type (TOTAL_INDICATOR_LIMIT / total_indicator_types)
    """
    result = calculate_limit_per_type(None, 4)
    expected = TOTAL_INDICATOR_LIMIT // 4  # 100,000 / 4 = 25,000
    assert result == expected
    assert result == 25000


def test_calculate_limit_per_type_with_negative():
    """
    Given:
        - limit is negative (-100)
        - total_indicator_types is 5
    When:
        - Calling calculate_limit_per_type
    Then:
        - Returns default limit per type (TOTAL_INDICATOR_LIMIT / total_indicator_types)
    """
    result = calculate_limit_per_type(-100, 5)
    expected = TOTAL_INDICATOR_LIMIT // 5  # 100,000 / 5 = 20,000
    assert result == expected
    assert result == 20000


def test_calculate_limit_per_type_exceeds_total():
    """
    Given:
        - limit is 30,000
        - total_indicator_types is 4
        - limit * types = 120,000 > TOTAL_INDICATOR_LIMIT (100,000)
    When:
        - Calling calculate_limit_per_type
    Then:
        - Returns default limit per type (TOTAL_INDICATOR_LIMIT / total_indicator_types)
    """
    result = calculate_limit_per_type(30000, 4)
    expected = TOTAL_INDICATOR_LIMIT // 4  # 100,000 / 4 = 25,000
    assert result == expected
    assert result == 25000


def test_calculate_limit_per_type_within_total():
    """
    Given:
        - limit is 20,000
        - total_indicator_types is 4
        - limit * types = 80,000 <= TOTAL_INDICATOR_LIMIT (100,000)
    When:
        - Calling calculate_limit_per_type
    Then:
        - Returns the provided limit
    """
    result = calculate_limit_per_type(20000, 4)
    assert result == 20000


def test_calculate_limit_per_type_exact_total():
    """
    Given:
        - limit is 25,000
        - total_indicator_types is 4
        - limit * types = 100,000 = TOTAL_INDICATOR_LIMIT
    When:
        - Calling calculate_limit_per_type
    Then:
        - Returns the provided limit (exactly at the limit)
    """
    result = calculate_limit_per_type(25000, 4)
    assert result == 25000


def test_calculate_limit_per_type_single_type():
    """
    Given:
        - limit is None
        - total_indicator_types is 1
    When:
        - Calling calculate_limit_per_type
    Then:
        - Returns TOTAL_INDICATOR_LIMIT (100,000 / 1 = 100,000)
    """
    result = calculate_limit_per_type(None, 1)
    assert result == TOTAL_INDICATOR_LIMIT
    assert result == 100000


def test_calculate_limit_per_type_many_types():
    """
    Given:
        - limit is None
        - total_indicator_types is 10
    When:
        - Calling calculate_limit_per_type
    Then:
        - Returns default limit per type (TOTAL_INDICATOR_LIMIT / 10 = 10,000)
    """
    result = calculate_limit_per_type(None, 10)
    expected = TOTAL_INDICATOR_LIMIT // 10  # 100,000 / 10 = 10,000
    assert result == expected
    assert result == 10000


def test_calculate_limit_per_type_small_limit():
    """
    Given:
        - limit is 5,000
        - total_indicator_types is 4
        - limit * types = 20,000 <= TOTAL_INDICATOR_LIMIT
    When:
        - Calling calculate_limit_per_type
    Then:
        - Returns the provided limit (5,000)
    """
    result = calculate_limit_per_type(5000, 4)
    assert result == 5000


def test_create_vulnerabilities_relationships_unknown_threat_class(mocker):
    """
    Given:
        - A threat object with an unknown threat_object_class (e.g. 'generic')
    When:
        - create_vulnerabilities_relationships is called
    Then:
        - Returns empty list without raising a KeyError
    """
    from Unit42Feed import create_vulnerabilities_relationships

    result = create_vulnerabilities_relationships({}, "SomeName", "generic")
    assert result == []


def test_create_actor_relationships_unknown_threat_class(mocker):
    """
    Given:
        - A threat object with an unknown threat_object_class (e.g. 'generic')
    When:
        - create_actor_relationships is called
    Then:
        - Returns empty list without raising a KeyError
    """
    from Unit42Feed import create_actor_relationships

    result = create_actor_relationships({}, "SomeName", "generic")
    assert result == []


def test_create_tools_relationships_unknown_threat_class(mocker):
    """
    Given:
        - A threat object with an unknown threat_object_class (e.g. 'generic')
    When:
        - create_tools_relationships is called
    Then:
        - Returns empty list without raising a KeyError
    """
    from Unit42Feed import create_tools_relationships

    result = create_tools_relationships({}, "SomeName", "generic")
    assert result == []


def test_create_malware_relationships_unknown_threat_class(mocker):
    """
    Given:
        - A threat object with an unknown threat_object_class (e.g. 'generic')
    When:
        - create_malware_relationships is called
    Then:
        - Returns empty list without raising a KeyError
    """
    from Unit42Feed import create_malware_relationships

    result = create_malware_relationships({}, "SomeName", "generic")
    assert result == []


def test_create_attack_patterns_relationships_unknown_threat_class(mocker):
    """
    Given:
        - A threat object with an unknown threat_object_class (e.g. 'generic')
    When:
        - create_attack_patterns_relationships is called
    Then:
        - Returns empty list without raising a KeyError
    """
    from Unit42Feed import create_attack_patterns_relationships

    result = create_attack_patterns_relationships({}, "SomeName", "generic")
    assert result == []


def test_create_campaigns_relationships_unknown_threat_class(mocker):
    """
    Given:
        - A threat object with an unknown threat_object_class (e.g. 'generic')
    When:
        - create_campaigns_relationships is called
    Then:
        - Returns empty list without raising a KeyError
    """
    from Unit42Feed import create_campaigns_relationships

    result = create_campaigns_relationships({}, "SomeName", "generic")
    assert result == []


def test_parse_threat_objects_unknown_threat_class(mocker):
    """
    Given:
        - A threat object with an unknown threat_object_class (e.g. 'malicious_tool')
          that was previously causing a KeyError crash
    When:
        - parse_threat_objects is called
    Then:
        - Does not raise a KeyError
        - Returns the threat object mapped to the correct XSOAR type (Tool)
    """
    from Unit42Feed import parse_threat_objects

    mock_demisto_params(mocker)

    threat_objects_data = [
        {"name": "ScreenConnect", "threat_object_class": "malicious_tool", "publications": []},
    ]

    result = parse_threat_objects(threat_objects_data)

    main_objects = [obj for obj in result if obj["type"] == ThreatIntel.ObjectsNames.TOOL]
    assert len(main_objects) == 1
    assert main_objects[0]["value"] == "ScreenConnect"