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
| ID | Unit 42 Feed |
|---|---|
| Provider | Palo Alto Networks |
| Category | Data Enrichment & Threat Intelligence |
| From Version | 6.10.0 |
| Docker Image | demisto/python3:3.12.13.10404775 |
| Supported Modules | Agentix 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:
- If the limit is not specified or is negative:
- Default limit per type =
100,000 / total_number_of_types
- Default limit per type =
- If the limit × total_number_of_types > 100,000:
- Adjusted limit per type =
100,000 / total_number_of_types
- Adjusted limit per type =
- 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,000per 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,000per 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,000per 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:
- Threat Objects (if enabled)
- IP indicators
- Domain indicators
- URL indicators
- 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/indicatorshttps://prod-us.tas.crtx.paloaltonetworks.com/api/v1/feeds/threat_objects
- This occurs when the proxy or firewall blocks outbound requests to the Unit 42 Feed API endpoint (
Configuration parameters
feed— Fetch indicatorsfeed_types— Feed Typesindicator_types— Indicator Typeslimit— Maximum Indicators Per FetchfeedFetchInterval— Feed Fetch IntervalfeedReputation— Indicator ReputationfeedReliability— Source ReliabilityfeedTags— Tagstlp_color— Traffic Light Protocol Color (TLP).feedExpirationPolicy—feedExpirationInterval— Indicator Expiration Intervalcreate_relationships— Create relationshipsfeedIncremental— Incremental feedfeedBypassExclusionList— Bypass exclusion listproxy— Use system proxy settingsinsecure— Trust any certificate (not secure)
Commands (2)
-
unit42-get-indicatorsGets indicators from the feed.
-
unit42-get-threat-objectsGets 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"