diff --git a/domaintools_iris.json b/domaintools_iris.json index e3358e2..401a45e 100644 --- a/domaintools_iris.json +++ b/domaintools_iris.json @@ -3298,6 +3298,1374 @@ } ], "versions": "EQ(*)" + }, + { + "action": "iris detect get new domains", + "description": "Retrieve newly discovered domains from Iris Detect across all monitors or a specific monitor", + "type": "investigate", + "identifier": "iris_detect_get_new_domains", + "read_only": true, + "parameters": { + "monitor_id": { + "description": "Monitor ID to filter results to a specific monitor", + "data_type": "string", + "order": 0 + }, + "tlds": { + "description": "Comma-separated list of TLDs to filter results (e.g. com,net)", + "data_type": "string", + "order": 1 + }, + "risk_score_ranges": { + "description": "Comma-separated risk score ranges to filter by (e.g. 70-99,100-100)", + "data_type": "string", + "order": 2 + }, + "mx_exists": { + "description": "Filter by whether the domain has an MX record", + "data_type": "boolean", + "order": 3 + }, + "discovered_since": { + "description": "Filter domains discovered since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 4 + }, + "changed_since": { + "description": "Filter domains changed since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 5 + }, + "search": { + "description": "Search string to filter domains by name (contains match)", + "data_type": "string", + "order": 6 + }, + "sort": { + "description": "Sort field (discovered_date, changed_date, or risk_score)", + "data_type": "string", + "value_list": [ + "discovered_date", + "changed_date", + "risk_score" + ], + "order": 7 + }, + "order": { + "description": "Sort order", + "data_type": "string", + "value_list": [ + "asc", + "desc" + ], + "order": 8 + }, + "include_domain_data": { + "description": "Include DNS and WHOIS/RDAP details in the response", + "data_type": "boolean", + "default": false, + "order": 9 + }, + "limit": { + "description": "Maximum number of results to return (max 100, or 50 if include_domain_data is true)", + "data_type": "numeric", + "order": 10 + }, + "preview": { + "description": "Preview mode for testing — limits results to 2 without hourly rate limit", + "data_type": "boolean", + "order": 11 + } + }, + "render": { + "width": 12, + "title": "Iris Detect New Domains", + "type": "table", + "height": 10 + }, + "output": [ + { + "data_path": "action_result.data.*.domain", + "data_type": "string", + "contains": ["domain"], + "column_name": "Domain", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.state", + "data_type": "string", + "column_name": "State", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.risk_score", + "data_type": "numeric", + "column_name": "Risk Score", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.risk_score_status", + "data_type": "string", + "column_name": "Risk Score Status", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.discovered_date", + "data_type": "string", + "column_name": "Discovered Date", + "column_order": 4 + }, + { + "data_path": "action_result.data.*.changed_date", + "data_type": "string", + "column_name": "Changed Date", + "column_order": 5 + }, + { + "data_path": "action_result.data.*.status", + "data_type": "string", + "column_name": "Status", + "column_order": 6 + }, + { + "data_path": "action_result.data.*.tld", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.mx_exists", + "data_type": "boolean" + }, + { + "data_path": "action_result.data.*.monitor_ids", + "data_type": "string" + }, + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": ["success", "failed"] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.summary.domain_count", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [1] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [1] + } + ], + "versions": "EQ(*)" + }, + { + "action": "iris detect get watched domains", + "description": "Retrieve watched domains from Iris Detect across all monitors or a specific monitor", + "type": "investigate", + "identifier": "iris_detect_get_watched_domains", + "read_only": true, + "parameters": { + "monitor_id": { + "description": "Monitor ID to filter results to a specific monitor", + "data_type": "string", + "order": 0 + }, + "tlds": { + "description": "Comma-separated list of TLDs to filter results (e.g. com,net)", + "data_type": "string", + "order": 1 + }, + "risk_score_ranges": { + "description": "Comma-separated risk score ranges to filter by (e.g. 70-99,100-100)", + "data_type": "string", + "order": 2 + }, + "mx_exists": { + "description": "Filter by whether the domain has an MX record", + "data_type": "boolean", + "order": 3 + }, + "discovered_since": { + "description": "Filter domains discovered since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 4 + }, + "changed_since": { + "description": "Filter domains changed since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 5 + }, + "escalated_since": { + "description": "Filter domains escalated since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 6 + }, + "search": { + "description": "Search string to filter domains by name (contains match)", + "data_type": "string", + "order": 7 + }, + "sort": { + "description": "Sort field", + "data_type": "string", + "value_list": [ + "discovered_date", + "changed_date", + "risk_score" + ], + "order": 8 + }, + "order": { + "description": "Sort order", + "data_type": "string", + "value_list": [ + "asc", + "desc" + ], + "order": 9 + }, + "include_domain_data": { + "description": "Include DNS and WHOIS/RDAP details in the response", + "data_type": "boolean", + "default": false, + "order": 10 + }, + "limit": { + "description": "Maximum number of results to return (max 100, or 50 if include_domain_data is true)", + "data_type": "numeric", + "order": 11 + }, + "preview": { + "description": "Preview mode for testing — limits results to 2 without hourly rate limit", + "data_type": "boolean", + "order": 12 + } + }, + "render": { + "width": 12, + "title": "Iris Detect Watched Domains", + "type": "table", + "height": 10 + }, + "output": [ + { + "data_path": "action_result.data.*.domain", + "data_type": "string", + "contains": ["domain"], + "column_name": "Domain", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.state", + "data_type": "string", + "column_name": "State", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.risk_score", + "data_type": "numeric", + "column_name": "Risk Score", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.risk_score_status", + "data_type": "string", + "column_name": "Risk Score Status", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.discovered_date", + "data_type": "string", + "column_name": "Discovered Date", + "column_order": 4 + }, + { + "data_path": "action_result.data.*.changed_date", + "data_type": "string", + "column_name": "Changed Date", + "column_order": 5 + }, + { + "data_path": "action_result.data.*.status", + "data_type": "string", + "column_name": "Status", + "column_order": 6 + }, + { + "data_path": "action_result.data.*.escalations", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.tld", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.mx_exists", + "data_type": "boolean" + }, + { + "data_path": "action_result.data.*.monitor_ids", + "data_type": "string" + }, + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": ["success", "failed"] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.summary.domain_count", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [1] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [1] + } + ], + "versions": "EQ(*)" + }, + { + "action": "iris detect get ignored domains", + "description": "Retrieve ignored (false positive) domains from Iris Detect across all monitors or a specific monitor", + "type": "investigate", + "identifier": "iris_detect_get_ignored_domains", + "read_only": true, + "parameters": { + "monitor_id": { + "description": "Monitor ID to filter results to a specific monitor", + "data_type": "string", + "order": 0 + }, + "tlds": { + "description": "Comma-separated list of TLDs to filter results (e.g. com,net)", + "data_type": "string", + "order": 1 + }, + "risk_score_ranges": { + "description": "Comma-separated risk score ranges to filter by (e.g. 70-99,100-100)", + "data_type": "string", + "order": 2 + }, + "mx_exists": { + "description": "Filter by whether the domain has an MX record", + "data_type": "boolean", + "order": 3 + }, + "discovered_since": { + "description": "Filter domains discovered since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 4 + }, + "changed_since": { + "description": "Filter domains changed since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 5 + }, + "escalated_since": { + "description": "Filter domains escalated since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 6 + }, + "search": { + "description": "Search string to filter domains by name (contains match)", + "data_type": "string", + "order": 7 + }, + "sort": { + "description": "Sort field", + "data_type": "string", + "value_list": [ + "discovered_date", + "changed_date", + "risk_score" + ], + "order": 8 + }, + "order": { + "description": "Sort order", + "data_type": "string", + "value_list": [ + "asc", + "desc" + ], + "order": 9 + }, + "include_domain_data": { + "description": "Include DNS and WHOIS/RDAP details in the response", + "data_type": "boolean", + "default": false, + "order": 10 + }, + "limit": { + "description": "Maximum number of results to return (max 100, or 50 if include_domain_data is true)", + "data_type": "numeric", + "order": 11 + }, + "preview": { + "description": "Preview mode for testing — limits results to 2 without hourly rate limit", + "data_type": "boolean", + "order": 12 + } + }, + "render": { + "width": 12, + "title": "Iris Detect Ignored Domains", + "type": "table", + "height": 10 + }, + "output": [ + { + "data_path": "action_result.data.*.domain", + "data_type": "string", + "contains": ["domain"], + "column_name": "Domain", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.state", + "data_type": "string", + "column_name": "State", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.risk_score", + "data_type": "numeric", + "column_name": "Risk Score", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.risk_score_status", + "data_type": "string", + "column_name": "Risk Score Status", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.discovered_date", + "data_type": "string", + "column_name": "Discovered Date", + "column_order": 4 + }, + { + "data_path": "action_result.data.*.changed_date", + "data_type": "string", + "column_name": "Changed Date", + "column_order": 5 + }, + { + "data_path": "action_result.data.*.status", + "data_type": "string", + "column_name": "Status", + "column_order": 6 + }, + { + "data_path": "action_result.data.*.tld", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.mx_exists", + "data_type": "boolean" + }, + { + "data_path": "action_result.data.*.monitor_ids", + "data_type": "string" + }, + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": ["success", "failed"] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.summary.domain_count", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [1] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [1] + } + ], + "versions": "EQ(*)" + }, + { + "action": "iris detect get escalated domains", + "description": "Retrieve domains escalated to Google Safe Browsing from Iris Detect", + "type": "investigate", + "identifier": "iris_detect_get_escalated_domains", + "read_only": true, + "parameters": { + "monitor_id": { + "description": "Monitor ID to filter results to a specific monitor", + "data_type": "string", + "order": 0 + }, + "tlds": { + "description": "Comma-separated list of TLDs to filter results (e.g. com,net)", + "data_type": "string", + "order": 1 + }, + "risk_score_ranges": { + "description": "Comma-separated risk score ranges to filter by (e.g. 70-99,100-100)", + "data_type": "string", + "order": 2 + }, + "mx_exists": { + "description": "Filter by whether the domain has an MX record", + "data_type": "boolean", + "order": 3 + }, + "discovered_since": { + "description": "Filter domains discovered since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 4 + }, + "changed_since": { + "description": "Filter domains changed since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 5 + }, + "escalated_since": { + "description": "Filter domains escalated since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 6 + }, + "search": { + "description": "Search string to filter domains by name (contains match)", + "data_type": "string", + "order": 7 + }, + "sort": { + "description": "Sort field", + "data_type": "string", + "value_list": [ + "discovered_date", + "changed_date", + "risk_score" + ], + "order": 8 + }, + "order": { + "description": "Sort order", + "data_type": "string", + "value_list": [ + "asc", + "desc" + ], + "order": 9 + }, + "include_domain_data": { + "description": "Include DNS and WHOIS/RDAP details in the response", + "data_type": "boolean", + "default": false, + "order": 10 + }, + "limit": { + "description": "Maximum number of results to return (max 100, or 50 if include_domain_data is true)", + "data_type": "numeric", + "order": 11 + }, + "preview": { + "description": "Preview mode for testing — limits results to 2 without hourly rate limit", + "data_type": "boolean", + "order": 12 + } + }, + "render": { + "width": 12, + "title": "Iris Detect Escalated Domains", + "type": "table", + "height": 10 + }, + "output": [ + { + "data_path": "action_result.data.*.domain", + "data_type": "string", + "contains": ["domain"], + "column_name": "Domain", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.state", + "data_type": "string", + "column_name": "State", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.risk_score", + "data_type": "numeric", + "column_name": "Risk Score", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.risk_score_status", + "data_type": "string", + "column_name": "Risk Score Status", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.discovered_date", + "data_type": "string", + "column_name": "Discovered Date", + "column_order": 4 + }, + { + "data_path": "action_result.data.*.changed_date", + "data_type": "string", + "column_name": "Changed Date", + "column_order": 5 + }, + { + "data_path": "action_result.data.*.status", + "data_type": "string", + "column_name": "Status", + "column_order": 6 + }, + { + "data_path": "action_result.data.*.escalations", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.tld", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.mx_exists", + "data_type": "boolean" + }, + { + "data_path": "action_result.data.*.monitor_ids", + "data_type": "string" + }, + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": ["success", "failed"] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.summary.domain_count", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [1] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [1] + } + ], + "versions": "EQ(*)" + }, + { + "action": "iris detect get blocklist domains", + "description": "Retrieve domains escalated for internal blocking from Iris Detect", + "type": "investigate", + "identifier": "iris_detect_get_blocklist_domains", + "read_only": true, + "parameters": { + "monitor_id": { + "description": "Monitor ID to filter results to a specific monitor", + "data_type": "string", + "order": 0 + }, + "tlds": { + "description": "Comma-separated list of TLDs to filter results (e.g. com,net)", + "data_type": "string", + "order": 1 + }, + "risk_score_ranges": { + "description": "Comma-separated risk score ranges to filter by (e.g. 70-99,100-100)", + "data_type": "string", + "order": 2 + }, + "mx_exists": { + "description": "Filter by whether the domain has an MX record", + "data_type": "boolean", + "order": 3 + }, + "discovered_since": { + "description": "Filter domains discovered since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 4 + }, + "changed_since": { + "description": "Filter domains changed since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 5 + }, + "escalated_since": { + "description": "Filter domains escalated since this datetime (ISO 8601 format)", + "data_type": "string", + "order": 6 + }, + "search": { + "description": "Search string to filter domains by name (contains match)", + "data_type": "string", + "order": 7 + }, + "sort": { + "description": "Sort field", + "data_type": "string", + "value_list": [ + "discovered_date", + "changed_date", + "risk_score" + ], + "order": 8 + }, + "order": { + "description": "Sort order", + "data_type": "string", + "value_list": [ + "asc", + "desc" + ], + "order": 9 + }, + "include_domain_data": { + "description": "Include DNS and WHOIS/RDAP details in the response", + "data_type": "boolean", + "default": false, + "order": 10 + }, + "limit": { + "description": "Maximum number of results to return (max 100, or 50 if include_domain_data is true)", + "data_type": "numeric", + "order": 11 + }, + "preview": { + "description": "Preview mode for testing — limits results to 2 without hourly rate limit", + "data_type": "boolean", + "order": 12 + } + }, + "render": { + "width": 12, + "title": "Iris Detect Blocklist Domains", + "type": "table", + "height": 10 + }, + "output": [ + { + "data_path": "action_result.data.*.domain", + "data_type": "string", + "contains": ["domain"], + "column_name": "Domain", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.state", + "data_type": "string", + "column_name": "State", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.risk_score", + "data_type": "numeric", + "column_name": "Risk Score", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.risk_score_status", + "data_type": "string", + "column_name": "Risk Score Status", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.discovered_date", + "data_type": "string", + "column_name": "Discovered Date", + "column_order": 4 + }, + { + "data_path": "action_result.data.*.changed_date", + "data_type": "string", + "column_name": "Changed Date", + "column_order": 5 + }, + { + "data_path": "action_result.data.*.status", + "data_type": "string", + "column_name": "Status", + "column_order": 6 + }, + { + "data_path": "action_result.data.*.escalations", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.tld", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.mx_exists", + "data_type": "boolean" + }, + { + "data_path": "action_result.data.*.monitor_ids", + "data_type": "string" + }, + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": ["success", "failed"] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.summary.domain_count", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [1] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [1] + } + ], + "versions": "EQ(*)" + }, + { + "action": "iris detect get monitors list", + "description": "Retrieve the list of monitors configured in Iris Detect for your account", + "type": "investigate", + "identifier": "iris_detect_get_monitors_list", + "read_only": true, + "parameters": { + "include_counts": { + "description": "Include counts of new, watched, changed, and escalated domains per monitor", + "data_type": "boolean", + "default": false, + "order": 0 + }, + "datetime_counts_since": { + "description": "Required if include_counts is true. Datetime to count domains from (ISO 8601 format)", + "data_type": "string", + "order": 1 + }, + "sort": { + "description": "Sort field", + "data_type": "string", + "value_list": [ + "term", + "created_date", + "domain_counts_changed", + "domain_counts_discovered" + ], + "order": 2 + }, + "order": { + "description": "Sort order", + "data_type": "string", + "value_list": [ + "asc", + "desc" + ], + "default": "desc", + "order": 3 + }, + "limit": { + "description": "Maximum number of monitors to return (max 500, or 100 if include_counts is true)", + "data_type": "numeric", + "order": 4 + } + }, + "render": { + "width": 12, + "title": "Iris Detect Monitors List", + "type": "table", + "height": 10 + }, + "output": [ + { + "data_path": "action_result.data.*.term", + "data_type": "string", + "column_name": "Term", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string", + "column_name": "Monitor ID", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.state", + "data_type": "string", + "column_name": "State", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.status", + "data_type": "string", + "column_name": "Status", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.created_date", + "data_type": "string", + "column_name": "Created Date", + "column_order": 4 + }, + { + "data_path": "action_result.data.*.updated_date", + "data_type": "string", + "column_name": "Updated Date", + "column_order": 5 + }, + { + "data_path": "action_result.data.*.created_by", + "data_type": "string", + "column_name": "Created By", + "column_order": 6 + }, + { + "data_path": "action_result.data.*.match_substring_variations", + "data_type": "boolean" + }, + { + "data_path": "action_result.data.*.nameserver_exclusions", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.text_exclusions", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.domain_counts.new", + "data_type": "numeric" + }, + { + "data_path": "action_result.data.*.domain_counts.watched", + "data_type": "numeric" + }, + { + "data_path": "action_result.data.*.domain_counts.changed", + "data_type": "numeric" + }, + { + "data_path": "action_result.data.*.domain_counts.escalated", + "data_type": "numeric" + }, + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": ["success", "failed"] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.summary.monitor_count", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [1] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [1] + } + ], + "versions": "EQ(*)" + }, + { + "action": "iris detect escalate domains", + "description": "Escalate one or more watched domains to Google Safe Browsing via Iris Detect", + "type": "correct", + "identifier": "iris_detect_escalate_domains", + "read_only": false, + "parameters": { + "watchlist_domain_ids": { + "description": "Comma-separated list of Iris Detect domain IDs to escalate to Google Safe Browsing", + "data_type": "string", + "required": true, + "primary": true, + "order": 0 + } + }, + "render": { + "width": 12, + "title": "Iris Detect Escalated Domains", + "type": "table", + "height": 10 + }, + "output": [ + { + "data_path": "action_result.data.*.watchlist_domain_id", + "data_type": "string", + "column_name": "Watchlist Domain ID", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.escalation_type", + "data_type": "string", + "column_name": "Escalation Type", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string", + "column_name": "Escalation ID", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.created_date", + "data_type": "string", + "column_name": "Created Date", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.created_by", + "data_type": "string", + "column_name": "Created By", + "column_order": 4 + }, + { + "data_path": "action_result.data.*.updated_date", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.watchlist_domain_ids", + "data_type": "string" + }, + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": ["success", "failed"] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.summary.escalated_count", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [1] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [1] + } + ], + "versions": "EQ(*)" + }, + { + "action": "iris detect blocklist domains", + "description": "Escalate one or more watched domains for internal blocking via Iris Detect", + "type": "correct", + "identifier": "iris_detect_blocklist_domains", + "read_only": false, + "parameters": { + "watchlist_domain_ids": { + "description": "Comma-separated list of Iris Detect domain IDs to escalate for internal blocking", + "data_type": "string", + "required": true, + "primary": true, + "order": 0 + } + }, + "render": { + "width": 12, + "title": "Iris Detect Blocklisted Domains", + "type": "table", + "height": 10 + }, + "output": [ + { + "data_path": "action_result.data.*.watchlist_domain_id", + "data_type": "string", + "column_name": "Watchlist Domain ID", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.escalation_type", + "data_type": "string", + "column_name": "Escalation Type", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string", + "column_name": "Escalation ID", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.created_date", + "data_type": "string", + "column_name": "Created Date", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.created_by", + "data_type": "string", + "column_name": "Created By", + "column_order": 4 + }, + { + "data_path": "action_result.data.*.updated_date", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.watchlist_domain_ids", + "data_type": "string" + }, + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": ["success", "failed"] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.summary.blocklisted_count", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [1] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [1] + } + ], + "versions": "EQ(*)" + }, + { + "action": "iris detect watch domains", + "description": "Add one or more domains to the Iris Detect watchlist for ongoing monitoring", + "type": "correct", + "identifier": "iris_detect_watch_domains", + "read_only": false, + "parameters": { + "watchlist_domain_ids": { + "description": "Comma-separated list of Iris Detect domain IDs to add to the watchlist", + "data_type": "string", + "required": true, + "primary": true, + "order": 0 + } + }, + "render": { + "width": 12, + "title": "Iris Detect Watched Domains", + "type": "table", + "height": 10 + }, + "output": [ + { + "data_path": "action_result.data.*.domain", + "data_type": "string", + "contains": ["domain"], + "column_name": "Domain", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.state", + "data_type": "string", + "column_name": "State", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string", + "column_name": "Domain ID", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.discovered_date", + "data_type": "string", + "column_name": "Discovered Date", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.changed_date", + "data_type": "string", + "column_name": "Changed Date", + "column_order": 4 + }, + { + "data_path": "action_result.parameter.watchlist_domain_ids", + "data_type": "string" + }, + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": ["success", "failed"] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.summary.watched_count", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [1] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [1] + } + ], + "versions": "EQ(*)" + }, + { + "action": "iris detect ignore domains", + "description": "Mark one or more domains as ignored (false positives) in Iris Detect", + "type": "correct", + "identifier": "iris_detect_ignore_domains", + "read_only": false, + "parameters": { + "watchlist_domain_ids": { + "description": "Comma-separated list of Iris Detect domain IDs to mark as ignored", + "data_type": "string", + "required": true, + "primary": true, + "order": 0 + } + }, + "render": { + "width": 12, + "title": "Iris Detect Ignored Domains", + "type": "table", + "height": 10 + }, + "output": [ + { + "data_path": "action_result.data.*.domain", + "data_type": "string", + "contains": ["domain"], + "column_name": "Domain", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.state", + "data_type": "string", + "column_name": "State", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.id", + "data_type": "string", + "column_name": "Domain ID", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.discovered_date", + "data_type": "string", + "column_name": "Discovered Date", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.changed_date", + "data_type": "string", + "column_name": "Changed Date", + "column_order": 4 + }, + { + "data_path": "action_result.parameter.watchlist_domain_ids", + "data_type": "string" + }, + { + "data_path": "action_result.status", + "data_type": "string", + "example_values": ["success", "failed"] + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "action_result.summary.ignored_count", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "example_values": [1] + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "example_values": [1] + } + ], + "versions": "EQ(*)" } ], "pip39_dependencies": { diff --git a/domaintools_iris_connector.py b/domaintools_iris_connector.py index 66c64d6..b6053f3 100644 --- a/domaintools_iris_connector.py +++ b/domaintools_iris_connector.py @@ -43,6 +43,19 @@ class DomainToolsConnector(BaseConnector): ACTION_ID_PARSED_DOMAIN_RDAP_FEED = "parsed_domain_rdap_feed" ACTION_ID_DOMAIN_RISK_FEED = "domain_risk_feed" ACTION_ID_DOMAIN_HOTLIST_FEED = "domain_hotlist_feed" + + # Iris Detect action_ids + ACTION_ID_IRIS_DETECT_GET_NEW_DOMAINS = "iris_detect_get_new_domains" + ACTION_ID_IRIS_DETECT_GET_WATCHED_DOMAINS = "iris_detect_get_watched_domains" + ACTION_ID_IRIS_DETECT_GET_IGNORED_DOMAINS = "iris_detect_get_ignored_domains" + ACTION_ID_IRIS_DETECT_GET_ESCALATED_DOMAINS = "iris_detect_get_escalated_domains" + ACTION_ID_IRIS_DETECT_GET_BLOCKLIST_DOMAINS = "iris_detect_get_blocklist_domains" + ACTION_ID_IRIS_DETECT_GET_MONITORS_LIST = "iris_detect_get_monitors_list" + ACTION_ID_IRIS_DETECT_ESCALATE_DOMAINS = "iris_detect_escalate_domains" + ACTION_ID_IRIS_DETECT_BLOCKLIST_DOMAINS = "iris_detect_blocklist_domains" + ACTION_ID_IRIS_DETECT_WATCH_DOMAINS = "iris_detect_watch_domains" + ACTION_ID_IRIS_DETECT_IGNORE_DOMAINS = "iris_detect_ignore_domains" + RTUF_SERVICES_LIST = [ "nod", "nad", @@ -83,6 +96,16 @@ def __init__(self): self.ACTION_ID_PARSED_DOMAIN_RDAP_FEED: self._parsed_domain_rdap_feed, self.ACTION_ID_DOMAIN_RISK_FEED: self._domain_risk_feed, self.ACTION_ID_DOMAIN_HOTLIST_FEED: self._domain_hotlist_feed, + self.ACTION_ID_IRIS_DETECT_GET_NEW_DOMAINS: self._iris_detect_get_new_domains, + self.ACTION_ID_IRIS_DETECT_GET_WATCHED_DOMAINS: self._iris_detect_get_watched_domains, + self.ACTION_ID_IRIS_DETECT_GET_IGNORED_DOMAINS: self._iris_detect_get_ignored_domains, + self.ACTION_ID_IRIS_DETECT_GET_ESCALATED_DOMAINS: self._iris_detect_get_escalated_domains, + self.ACTION_ID_IRIS_DETECT_GET_BLOCKLIST_DOMAINS: self._iris_detect_get_blocklist_domains, + self.ACTION_ID_IRIS_DETECT_GET_MONITORS_LIST: self._iris_detect_get_monitors_list, + self.ACTION_ID_IRIS_DETECT_ESCALATE_DOMAINS: self._iris_detect_escalate_domains, + self.ACTION_ID_IRIS_DETECT_BLOCKLIST_DOMAINS: self._iris_detect_blocklist_domains, + self.ACTION_ID_IRIS_DETECT_WATCH_DOMAINS: self._iris_detect_watch_domains, + self.ACTION_ID_IRIS_DETECT_IGNORE_DOMAINS: self._iris_detect_ignore_domains, } def initialize(self): @@ -983,6 +1006,245 @@ def _get_rtuf_actions_params(self, param): return params + def _get_dt_api(self): + return API( + self._username, + self._key, + app_partner=self.app_partner, + app_name=self.app_name, + app_version=self.app_version_number, + proxy_url=self._proxy_url, + verify_ssl=self._ssl, + https=self._ssl if isinstance(self._ssl, bool) else True, + ) + + def _parse_detect_response(self, action_result, results, summary_key="domain_count"): + data = list(results) + action_result.update_data(data) + action_result.update_summary({summary_key: len(data)}) + return action_result.set_status(phantom.APP_SUCCESS) + + def _detect_error(self, action_result, e): + error_code, error_msg = self._get_error_message_from_exception(e) + return action_result.set_status(phantom.APP_ERROR, f"Error Code: {error_code}. Error Message: {error_msg}") + + def _iris_detect_get_new_domains(self, param): + self.save_progress(f"Starting {self.ACTION_ID_IRIS_DETECT_GET_NEW_DOMAINS} action.") + action_result = self.add_action_result(ActionResult(dict(param))) + try: + dt_api = self._get_dt_api() + results = dt_api.iris_detect_new_domains( + monitor_id=param.get("monitor_id"), + tlds=param.get("tlds"), + risk_score_ranges=param.get("risk_score_ranges"), + mx_exists=param.get("mx_exists"), + discovered_since=param.get("discovered_since"), + changed_since=param.get("changed_since"), + search=param.get("search"), + sort=param.get("sort"), + order=param.get("order"), + include_domain_data=param.get("include_domain_data", False), + limit=param.get("limit"), + preview=param.get("preview"), + ) + ret_val = self._parse_detect_response(action_result, results) + self.save_progress(f"Completed {self.ACTION_ID_IRIS_DETECT_GET_NEW_DOMAINS} action.") + return ret_val + except Exception as e: + return self._detect_error(action_result, e) + + def _iris_detect_get_watched_domains(self, param): + self.save_progress(f"Starting {self.ACTION_ID_IRIS_DETECT_GET_WATCHED_DOMAINS} action.") + action_result = self.add_action_result(ActionResult(dict(param))) + try: + dt_api = self._get_dt_api() + results = dt_api.iris_detect_watched_domains( + monitor_id=param.get("monitor_id"), + tlds=param.get("tlds"), + risk_score_ranges=param.get("risk_score_ranges"), + mx_exists=param.get("mx_exists"), + discovered_since=param.get("discovered_since"), + changed_since=param.get("changed_since"), + escalated_since=param.get("escalated_since"), + search=param.get("search"), + sort=param.get("sort"), + order=param.get("order"), + include_domain_data=param.get("include_domain_data", False), + limit=param.get("limit"), + preview=param.get("preview"), + ) + ret_val = self._parse_detect_response(action_result, results) + self.save_progress(f"Completed {self.ACTION_ID_IRIS_DETECT_GET_WATCHED_DOMAINS} action.") + return ret_val + except Exception as e: + return self._detect_error(action_result, e) + + def _iris_detect_get_ignored_domains(self, param): + self.save_progress(f"Starting {self.ACTION_ID_IRIS_DETECT_GET_IGNORED_DOMAINS} action.") + action_result = self.add_action_result(ActionResult(dict(param))) + try: + dt_api = self._get_dt_api() + results = dt_api.iris_detect_ignored_domains( + monitor_id=param.get("monitor_id"), + tlds=param.get("tlds"), + risk_score_ranges=param.get("risk_score_ranges"), + mx_exists=param.get("mx_exists"), + discovered_since=param.get("discovered_since"), + changed_since=param.get("changed_since"), + escalated_since=param.get("escalated_since"), + search=param.get("search"), + sort=param.get("sort"), + order=param.get("order"), + include_domain_data=param.get("include_domain_data", False), + limit=param.get("limit"), + preview=param.get("preview"), + ) + ret_val = self._parse_detect_response(action_result, results) + self.save_progress(f"Completed {self.ACTION_ID_IRIS_DETECT_GET_IGNORED_DOMAINS} action.") + return ret_val + except Exception as e: + return self._detect_error(action_result, e) + + def _iris_detect_get_escalated_domains(self, param): + self.save_progress(f"Starting {self.ACTION_ID_IRIS_DETECT_GET_ESCALATED_DOMAINS} action.") + action_result = self.add_action_result(ActionResult(dict(param))) + try: + dt_api = self._get_dt_api() + results = dt_api.iris_detect_watched_domains( + monitor_id=param.get("monitor_id"), + escalation_types=["google_safe"], + tlds=param.get("tlds"), + risk_score_ranges=param.get("risk_score_ranges"), + mx_exists=param.get("mx_exists"), + discovered_since=param.get("discovered_since"), + changed_since=param.get("changed_since"), + escalated_since=param.get("escalated_since"), + search=param.get("search"), + sort=param.get("sort"), + order=param.get("order"), + include_domain_data=param.get("include_domain_data", False), + limit=param.get("limit"), + preview=param.get("preview"), + ) + ret_val = self._parse_detect_response(action_result, results) + self.save_progress(f"Completed {self.ACTION_ID_IRIS_DETECT_GET_ESCALATED_DOMAINS} action.") + return ret_val + except Exception as e: + return self._detect_error(action_result, e) + + def _iris_detect_get_blocklist_domains(self, param): + self.save_progress(f"Starting {self.ACTION_ID_IRIS_DETECT_GET_BLOCKLIST_DOMAINS} action.") + action_result = self.add_action_result(ActionResult(dict(param))) + try: + dt_api = self._get_dt_api() + results = dt_api.iris_detect_watched_domains( + monitor_id=param.get("monitor_id"), + escalation_types=["blocked"], + tlds=param.get("tlds"), + risk_score_ranges=param.get("risk_score_ranges"), + mx_exists=param.get("mx_exists"), + discovered_since=param.get("discovered_since"), + changed_since=param.get("changed_since"), + escalated_since=param.get("escalated_since"), + search=param.get("search"), + sort=param.get("sort"), + order=param.get("order"), + include_domain_data=param.get("include_domain_data", False), + limit=param.get("limit"), + preview=param.get("preview"), + ) + ret_val = self._parse_detect_response(action_result, results) + self.save_progress(f"Completed {self.ACTION_ID_IRIS_DETECT_GET_BLOCKLIST_DOMAINS} action.") + return ret_val + except Exception as e: + return self._detect_error(action_result, e) + + def _iris_detect_get_monitors_list(self, param): + self.save_progress(f"Starting {self.ACTION_ID_IRIS_DETECT_GET_MONITORS_LIST} action.") + action_result = self.add_action_result(ActionResult(dict(param))) + try: + dt_api = self._get_dt_api() + include_counts = param.get("include_counts", False) + kwargs = {} + if include_counts: + kwargs["include_counts"] = True + kwargs["datetime_counts_since"] = param.get("datetime_counts_since") + results = dt_api.iris_detect_monitors( + sort=param.get("sort"), + order=param.get("order", "desc"), + limit=param.get("limit"), + **kwargs, + ) + ret_val = self._parse_detect_response(action_result, results, summary_key="monitor_count") + self.save_progress(f"Completed {self.ACTION_ID_IRIS_DETECT_GET_MONITORS_LIST} action.") + return ret_val + except Exception as e: + return self._detect_error(action_result, e) + + def _iris_detect_escalate_domains(self, param): + self.save_progress(f"Starting {self.ACTION_ID_IRIS_DETECT_ESCALATE_DOMAINS} action.") + action_result = self.add_action_result(ActionResult(dict(param))) + try: + dt_api = self._get_dt_api() + watchlist_domain_ids = param.get("watchlist_domain_ids", "").replace(" ", "").split(",") + results = dt_api.iris_detect_escalate_domains( + watchlist_domain_ids=watchlist_domain_ids, + escalation_type="google_safe", + ) + ret_val = self._parse_detect_response(action_result, results, summary_key="escalated_count") + self.save_progress(f"Completed {self.ACTION_ID_IRIS_DETECT_ESCALATE_DOMAINS} action.") + return ret_val + except Exception as e: + return self._detect_error(action_result, e) + + def _iris_detect_blocklist_domains(self, param): + self.save_progress(f"Starting {self.ACTION_ID_IRIS_DETECT_BLOCKLIST_DOMAINS} action.") + action_result = self.add_action_result(ActionResult(dict(param))) + try: + dt_api = self._get_dt_api() + watchlist_domain_ids = param.get("watchlist_domain_ids", "").replace(" ", "").split(",") + results = dt_api.iris_detect_escalate_domains( + watchlist_domain_ids=watchlist_domain_ids, + escalation_type="blocked", + ) + ret_val = self._parse_detect_response(action_result, results, summary_key="blocklisted_count") + self.save_progress(f"Completed {self.ACTION_ID_IRIS_DETECT_BLOCKLIST_DOMAINS} action.") + return ret_val + except Exception as e: + return self._detect_error(action_result, e) + + def _iris_detect_watch_domains(self, param): + self.save_progress(f"Starting {self.ACTION_ID_IRIS_DETECT_WATCH_DOMAINS} action.") + action_result = self.add_action_result(ActionResult(dict(param))) + try: + dt_api = self._get_dt_api() + watchlist_domain_ids = param.get("watchlist_domain_ids", "").replace(" ", "").split(",") + results = dt_api.iris_detect_manage_watchlist_domains( + watchlist_domain_ids=watchlist_domain_ids, + state="watched", + ) + ret_val = self._parse_detect_response(action_result, results, summary_key="watched_count") + self.save_progress(f"Completed {self.ACTION_ID_IRIS_DETECT_WATCH_DOMAINS} action.") + return ret_val + except Exception as e: + return self._detect_error(action_result, e) + + def _iris_detect_ignore_domains(self, param): + self.save_progress(f"Starting {self.ACTION_ID_IRIS_DETECT_IGNORE_DOMAINS} action.") + action_result = self.add_action_result(ActionResult(dict(param))) + try: + dt_api = self._get_dt_api() + watchlist_domain_ids = param.get("watchlist_domain_ids", "").replace(" ", "").split(",") + results = dt_api.iris_detect_manage_watchlist_domains( + watchlist_domain_ids=watchlist_domain_ids, + state="ignored", + ) + ret_val = self._parse_detect_response(action_result, results, summary_key="ignored_count") + self.save_progress(f"Completed {self.ACTION_ID_IRIS_DETECT_IGNORE_DOMAINS} action.") + return ret_val + except Exception as e: + return self._detect_error(action_result, e) + if __name__ == "__main__": import argparse diff --git a/pyproject.toml b/pyproject.toml index a816e31..09dbbdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,11 @@ +# Test configuration +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] +addopts = "-v --tb=short" + # Ruff linting [tool.ruff] line-length = 145 diff --git a/requirements-test.txt b/requirements-test.txt new file mode 100644 index 0000000..d55cb94 --- /dev/null +++ b/requirements-test.txt @@ -0,0 +1,4 @@ +pytest>=7.4 +pytest-mock>=3.12 +domaintools_api==2.7.0 +tldextract==5.3.0 diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..112d2fa --- /dev/null +++ b/tests/README.md @@ -0,0 +1,76 @@ +# Running Tests + +## Setup + +Create and activate a virtual environment, then install test dependencies: + +```bash +python3 -m venv .venv +source .venv/bin/activate +pip install -r requirements-test.txt +``` + +> The `.venv` directory is already created if you have run tests before. Just activate it. + +## Running Tests + +### All tests +```bash +.venv/bin/python -m pytest +``` + +### A specific test file +```bash +.venv/bin/python -m pytest tests/test_iris_detect_get_new_domains.py +``` + +### A specific test case +```bash +.venv/bin/python -m pytest tests/test_iris_detect_get_new_domains.py::TestIrisDetectGetNewDomains::test_returns_success_with_results +``` + +### With verbose output +```bash +.venv/bin/python -m pytest -v +``` + +### Stop on first failure +```bash +.venv/bin/python -m pytest -x +``` + +## Test Files + +| File | Action Tested | +|---|---| +| `test_iris_detect_get_new_domains.py` | `_iris_detect_get_new_domains` | +| `test_iris_detect_get_watched_domains.py` | `_iris_detect_get_watched_domains` | +| `test_iris_detect_get_ignored_domains.py` | `_iris_detect_get_ignored_domains` | +| `test_iris_detect_get_escalated_domains.py` | `_iris_detect_get_escalated_domains` | +| `test_iris_detect_get_blocklist_domains.py` | `_iris_detect_get_blocklist_domains` | +| `test_iris_detect_get_monitors_list.py` | `_iris_detect_get_monitors_list` | +| `test_iris_detect_escalate_domains.py` | `_iris_detect_escalate_domains` | +| `test_iris_detect_blocklist_domains.py` | `_iris_detect_blocklist_domains` | +| `test_iris_detect_watch_domains.py` | `_iris_detect_watch_domains` | +| `test_iris_detect_ignore_domains.py` | `_iris_detect_ignore_domains` | + +## How It Works + +The `phantom.*` packages are only available inside a real Splunk SOAR instance. The `conftest.py` file stubs out the entire `phantom` namespace before the connector is imported, allowing tests to run locally without a SOAR instance. + +The `domaintools.API` wrapper is patched via the `mock_dt_api` fixture in `conftest.py`, so no real API credentials or network access are needed. + +### Key fixtures (defined in `conftest.py`) + +| Fixture | Description | +|---|---| +| `connector` | A pre-configured `DomainToolsConnector` instance | +| `mock_dt_api` | Patches `_get_dt_api` and returns a `MagicMock` API instance | + +### Response helpers (defined in `conftest.py`) + +| Helper | Returns | +|---|---| +| `make_domain(...)` | A fake domain dict matching the Iris Detect API response shape | +| `make_monitor(...)` | A fake monitor dict | +| `make_escalation(...)` | A fake escalation dict | diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..f02e493 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,233 @@ +""" +Shared fixtures and phantom module stubs for DomainTools connector tests. + +phantom.* packages are only available inside a real Splunk SOAR instance. +We stub the minimum surface area needed to import and exercise the connector. +""" + +import sys +from types import ModuleType +from unittest.mock import MagicMock, patch + +import pytest + + +# --------------------------------------------------------------------------- +# Stub out the phantom.* namespace before the connector is imported +# --------------------------------------------------------------------------- + +def _make_phantom_stub(): + phantom = ModuleType("phantom") + phantom.APP_SUCCESS = True + phantom.APP_ERROR = False + phantom.APP_PROG_CONNECTING_TO_ELLIPSES = "Connecting to %s..." + phantom.ACTION_ID_TEST_ASSET_CONNECTIVITY = "test_asset_connectivity" + + # phantom.app sub-module + app = ModuleType("phantom.app") + app.APP_SUCCESS = True + app.APP_ERROR = False + app.APP_PROG_CONNECTING_TO_ELLIPSES = "Connecting to %s..." + app.ACTION_ID_TEST_ASSET_CONNECTIVITY = "test_asset_connectivity" + phantom.app = app + + # phantom.action_result sub-module + ar_mod = ModuleType("phantom.action_result") + + class ActionResult: + def __init__(self, param=None): + self._param = param or {} + self._data = [] + self._status = None + self._message = "" + self._summary = {} + + def set_status(self, status, message="", *args): + self._status = status + self._message = str(message) + return status + + def get_status(self): + return self._status + + def get_message(self): + return self._message + + def add_data(self, data): + self._data.append(data) + + def update_data(self, data): + self._data.extend(data) + + def get_data(self): + return self._data + + def update_summary(self, summary): + self._summary.update(summary) + + def get_summary(self): + return self._summary + + ar_mod.ActionResult = ActionResult + phantom.action_result = ar_mod + + # phantom.base_connector sub-module + bc_mod = ModuleType("phantom.base_connector") + + class BaseConnector: + def __init__(self): + self._action_results = [] + + def add_action_result(self, ar): + self._action_results.append(ar) + return ar + + def last_action_result(self): + return self._action_results[-1] if self._action_results else None + + def get_action_identifier(self): + return "" + + def get_app_json(self): + return {"app_version": "1.8.0", "name": "DomainTools Iris Investigate"} + + def get_phantom_base_url(self): + return "https://soar.local" + + def get_config(self): + return {} + + def save_progress(self, msg, *args): + pass + + def debug_print(self, msg, *args): + pass + + def set_status_save_progress(self, status, msg): + return status + + def set_status(self, status, msg="", *args): + return status + + bc_mod.BaseConnector = BaseConnector + phantom.base_connector = bc_mod + + # phantom.requests stub + requests_stub = MagicMock() + phantom.requests = requests_stub + + # phantom.rules stub (used in playbooks, not connector — included for completeness) + rules_stub = MagicMock() + phantom.rules = rules_stub + + return phantom + + +# Install stubs into sys.modules before any connector import +_phantom = _make_phantom_stub() +sys.modules.setdefault("phantom", _phantom) +sys.modules.setdefault("phantom.app", _phantom.app) +sys.modules.setdefault("phantom.action_result", _phantom.action_result) +sys.modules.setdefault("phantom.base_connector", _phantom.base_connector) +sys.modules.setdefault("phantom.requests", _phantom.requests) +sys.modules.setdefault("phantom.rules", _phantom.rules) + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +@pytest.fixture() +def connector(): + """A DomainToolsConnector with minimal config pre-loaded.""" + from domaintools_iris_connector import DomainToolsConnector + + conn = DomainToolsConnector() + conn._username = "test_user" + conn._key = "test_key" + conn._ssl = True + conn._proxy_url = None + conn.app_partner = "splunk_soar" + conn.app_name = "DomainTools Iris Investigate" + conn.app_version_number = "1.8.0" + return conn + + +@pytest.fixture() +def mock_dt_api(mocker): + """Patches _get_dt_api on the connector and returns the mock API instance.""" + mock_api = MagicMock() + mocker.patch( + "domaintools_iris_connector.DomainToolsConnector._get_dt_api", + return_value=mock_api, + ) + return mock_api + + +# --------------------------------------------------------------------------- +# Sample API response helpers +# --------------------------------------------------------------------------- + +def make_domain( + domain="evil-example.com", + state="new", + risk_score=85, + domain_id="abc123", + discovered_date="2026-01-01T00:00:00Z", + changed_date="2026-01-02T00:00:00Z", + status="active", + tld="com", + mx_exists=False, + escalations=None, + monitor_ids=None, +): + return { + "domain": domain, + "state": state, + "risk_score": risk_score, + "risk_score_status": "full", + "id": domain_id, + "discovered_date": discovered_date, + "changed_date": changed_date, + "status": status, + "tld": tld, + "mx_exists": mx_exists, + "escalations": escalations or [], + "monitor_ids": monitor_ids or ["mon1"], + } + + +def make_monitor( + term="domaintools", + monitor_id="mon1", + state="active", + status="completed", + created_by="test_user", +): + return { + "term": term, + "id": monitor_id, + "state": state, + "status": status, + "created_date": "2026-01-01T00:00:00Z", + "updated_date": "2026-01-01T00:00:00Z", + "created_by": created_by, + "match_substring_variations": False, + "nameserver_exclusions": [], + "text_exclusions": [], + } + + +def make_escalation( + watchlist_domain_id="abc123", + escalation_type="blocked", + escalation_id="esc1", +): + return { + "watchlist_domain_id": watchlist_domain_id, + "escalation_type": escalation_type, + "id": escalation_id, + "created_date": "2026-01-01T00:00:00Z", + "updated_date": "2026-01-01T00:00:00Z", + "created_by": "test_user", + } diff --git a/tests/test_iris_detect_blocklist_domains.py b/tests/test_iris_detect_blocklist_domains.py new file mode 100644 index 0000000..7b96c0f --- /dev/null +++ b/tests/test_iris_detect_blocklist_domains.py @@ -0,0 +1,107 @@ +"""Unit tests for _iris_detect_blocklist_domains action.""" + +from tests.conftest import make_escalation + + +class TestIrisDetectBlocklistDomains: + + def test_returns_success_with_results(self, connector, mock_dt_api): + escalations = [make_escalation("id1", "blocked", "b1"), make_escalation("id2", "blocked", "b2")] + mock_dt_api.iris_detect_escalate_domains.return_value = iter(escalations) + + result = connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "id1,id2"}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_status() is True + assert ar.get_summary()["blocklisted_count"] == 2 + assert len(ar.get_data()) == 2 + + def test_returns_success_with_single_id(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([make_escalation("id1", "blocked", "b1")]) + + result = connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_summary()["blocklisted_count"] == 1 + + def test_always_uses_blocked_escalation_type(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([]) + + connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "id1"}) + + _, kwargs = mock_dt_api.iris_detect_escalate_domains.call_args + assert kwargs["escalation_type"] == "blocked" + + def test_passes_single_domain_id_as_list(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([]) + + connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "abc123"}) + + _, kwargs = mock_dt_api.iris_detect_escalate_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["abc123"] + + def test_passes_multiple_domain_ids_as_list(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([]) + + connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "id1,id2,id3"}) + + _, kwargs = mock_dt_api.iris_detect_escalate_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["id1", "id2", "id3"] + + def test_strips_spaces_from_domain_ids(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([]) + + connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "id1, id2, id3"}) + + _, kwargs = mock_dt_api.iris_detect_escalate_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["id1", "id2", "id3"] + + def test_blocklist_data_stored_correctly(self, connector, mock_dt_api): + escalation = make_escalation("dom1", "blocked", "blk99") + mock_dt_api.iris_detect_escalate_domains.return_value = iter([escalation]) + + connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "dom1"}) + ar = connector.last_action_result() + + stored = ar.get_data()[0] + assert stored["watchlist_domain_id"] == "dom1" + assert stored["escalation_type"] == "blocked" + assert stored["id"] == "blk99" + + def test_summary_key_is_blocklisted_count(self, connector, mock_dt_api): + escalations = [make_escalation(f"id{i}", "blocked", f"b{i}") for i in range(4)] + mock_dt_api.iris_detect_escalate_domains.return_value = iter(escalations) + + connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "id0,id1,id2,id3"}) + ar = connector.last_action_result() + + assert "blocklisted_count" in ar.get_summary() + assert ar.get_summary()["blocklisted_count"] == 4 + + def test_blocked_not_same_as_google_safe(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([]) + + connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "id1"}) + + _, kwargs = mock_dt_api.iris_detect_escalate_domains.call_args + assert kwargs["escalation_type"] != "google_safe" + + def test_returns_error_on_api_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.side_effect = Exception("timeout") + + result = connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert result is False + assert "timeout" in ar.get_message() + + def test_returns_error_with_code_on_structured_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.side_effect = Exception(401, "Unauthorized") + + connector._iris_detect_blocklist_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert "401" in ar.get_message() + assert "Unauthorized" in ar.get_message() diff --git a/tests/test_iris_detect_escalate_domains.py b/tests/test_iris_detect_escalate_domains.py new file mode 100644 index 0000000..17a9856 --- /dev/null +++ b/tests/test_iris_detect_escalate_domains.py @@ -0,0 +1,99 @@ +"""Unit tests for _iris_detect_escalate_domains action.""" + +from tests.conftest import make_escalation + + +class TestIrisDetectEscalateDomains: + + def test_returns_success_with_results(self, connector, mock_dt_api): + escalations = [make_escalation("id1", "google_safe", "e1"), make_escalation("id2", "google_safe", "e2")] + mock_dt_api.iris_detect_escalate_domains.return_value = iter(escalations) + + result = connector._iris_detect_escalate_domains({"watchlist_domain_ids": "id1,id2"}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_status() is True + assert ar.get_summary()["escalated_count"] == 2 + assert len(ar.get_data()) == 2 + + def test_returns_success_with_single_id(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([make_escalation("id1", "google_safe", "e1")]) + + result = connector._iris_detect_escalate_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_summary()["escalated_count"] == 1 + + def test_always_uses_google_safe_escalation_type(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([]) + + connector._iris_detect_escalate_domains({"watchlist_domain_ids": "id1"}) + + _, kwargs = mock_dt_api.iris_detect_escalate_domains.call_args + assert kwargs["escalation_type"] == "google_safe" + + def test_passes_single_domain_id_as_list(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([]) + + connector._iris_detect_escalate_domains({"watchlist_domain_ids": "abc123"}) + + _, kwargs = mock_dt_api.iris_detect_escalate_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["abc123"] + + def test_passes_multiple_domain_ids_as_list(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([]) + + connector._iris_detect_escalate_domains({"watchlist_domain_ids": "id1,id2,id3"}) + + _, kwargs = mock_dt_api.iris_detect_escalate_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["id1", "id2", "id3"] + + def test_strips_spaces_from_domain_ids(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.return_value = iter([]) + + connector._iris_detect_escalate_domains({"watchlist_domain_ids": "id1, id2, id3"}) + + _, kwargs = mock_dt_api.iris_detect_escalate_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["id1", "id2", "id3"] + + def test_escalation_data_stored_correctly(self, connector, mock_dt_api): + escalation = make_escalation("dom1", "google_safe", "esc99") + mock_dt_api.iris_detect_escalate_domains.return_value = iter([escalation]) + + connector._iris_detect_escalate_domains({"watchlist_domain_ids": "dom1"}) + ar = connector.last_action_result() + + stored = ar.get_data()[0] + assert stored["watchlist_domain_id"] == "dom1" + assert stored["escalation_type"] == "google_safe" + assert stored["id"] == "esc99" + + def test_summary_key_is_escalated_count(self, connector, mock_dt_api): + escalations = [make_escalation(f"id{i}", "google_safe", f"e{i}") for i in range(3)] + mock_dt_api.iris_detect_escalate_domains.return_value = iter(escalations) + + connector._iris_detect_escalate_domains({"watchlist_domain_ids": "id0,id1,id2"}) + ar = connector.last_action_result() + + assert "escalated_count" in ar.get_summary() + assert ar.get_summary()["escalated_count"] == 3 + + def test_returns_error_on_api_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.side_effect = Exception("API error") + + result = connector._iris_detect_escalate_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert result is False + assert "API error" in ar.get_message() + + def test_returns_error_with_code_on_structured_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_escalate_domains.side_effect = Exception(403, "Forbidden") + + connector._iris_detect_escalate_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert "403" in ar.get_message() + assert "Forbidden" in ar.get_message() diff --git a/tests/test_iris_detect_get_blocklist_domains.py b/tests/test_iris_detect_get_blocklist_domains.py new file mode 100644 index 0000000..067bbd1 --- /dev/null +++ b/tests/test_iris_detect_get_blocklist_domains.py @@ -0,0 +1,119 @@ +"""Unit tests for _iris_detect_get_blocklist_domains action.""" + +from tests.conftest import make_domain + + +class TestIrisDetectGetBlocklistDomains: + + def test_returns_success_with_results(self, connector, mock_dt_api): + domains = [ + make_domain("block1.com", state="watched", domain_id="id1", escalations=[{"escalation_type": "blocked", "id": "b1"}]), + make_domain("block2.net", state="watched", domain_id="id2", escalations=[{"escalation_type": "blocked", "id": "b2"}]), + ] + mock_dt_api.iris_detect_watched_domains.return_value = iter(domains) + + result = connector._iris_detect_get_blocklist_domains({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_status() is True + assert ar.get_summary()["domain_count"] == 2 + assert len(ar.get_data()) == 2 + + def test_returns_success_with_no_results(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + result = connector._iris_detect_get_blocklist_domains({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_summary()["domain_count"] == 0 + + def test_always_filters_blocked(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + connector._iris_detect_get_blocklist_domains({}) + + _, kwargs = mock_dt_api.iris_detect_watched_domains.call_args + assert kwargs["escalation_types"] == ["blocked"] + + def test_blocked_not_overridable_by_caller(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + # caller passes nothing — escalation_types must still be ["blocked"] + connector._iris_detect_get_blocklist_domains({"monitor_id": "mon1"}) + + _, kwargs = mock_dt_api.iris_detect_watched_domains.call_args + assert kwargs["escalation_types"] == ["blocked"] + + def test_passes_additional_params(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + connector._iris_detect_get_blocklist_domains({ + "monitor_id": "mon999", + "tlds": "io", + "risk_score_ranges": "100-100", + "mx_exists": False, + "discovered_since": "2026-02-01T00:00:00Z", + "changed_since": "2026-02-02T00:00:00Z", + "escalated_since": "2026-02-03T00:00:00Z", + "search": "threat", + "sort": "risk_score", + "order": "desc", + "include_domain_data": True, + "limit": 50, + "preview": False, + }) + + mock_dt_api.iris_detect_watched_domains.assert_called_once_with( + monitor_id="mon999", + escalation_types=["blocked"], + tlds="io", + risk_score_ranges="100-100", + mx_exists=False, + discovered_since="2026-02-01T00:00:00Z", + changed_since="2026-02-02T00:00:00Z", + escalated_since="2026-02-03T00:00:00Z", + search="threat", + sort="risk_score", + order="desc", + include_domain_data=True, + limit=50, + preview=False, + ) + + def test_domain_data_stored_correctly(self, connector, mock_dt_api): + domain = make_domain( + "blocked.com", + state="watched", + risk_score=100, + domain_id="blk1", + escalations=[{"escalation_type": "blocked", "id": "b99", "created": "2026-01-01T00:00:00Z"}], + ) + mock_dt_api.iris_detect_watched_domains.return_value = iter([domain]) + + connector._iris_detect_get_blocklist_domains({}) + ar = connector.last_action_result() + + stored = ar.get_data()[0] + assert stored["domain"] == "blocked.com" + assert stored["risk_score"] == 100 + assert stored["escalations"][0]["escalation_type"] == "blocked" + + def test_returns_error_on_api_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.side_effect = Exception("network error") + + result = connector._iris_detect_get_blocklist_domains({}) + ar = connector.last_action_result() + + assert result is False + assert "network error" in ar.get_message() + + def test_returns_error_with_code_on_structured_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.side_effect = Exception(401, "Unauthorized") + + connector._iris_detect_get_blocklist_domains({}) + ar = connector.last_action_result() + + assert "401" in ar.get_message() + assert "Unauthorized" in ar.get_message() diff --git a/tests/test_iris_detect_get_escalated_domains.py b/tests/test_iris_detect_get_escalated_domains.py new file mode 100644 index 0000000..55ed39e --- /dev/null +++ b/tests/test_iris_detect_get_escalated_domains.py @@ -0,0 +1,119 @@ +"""Unit tests for _iris_detect_get_escalated_domains action.""" + +from tests.conftest import make_domain + + +class TestIrisDetectGetEscalatedDomains: + + def test_returns_success_with_results(self, connector, mock_dt_api): + domains = [ + make_domain("evil.com", state="watched", domain_id="id1", escalations=[{"escalation_type": "google_safe", "id": "e1"}]), + make_domain("phish.net", state="watched", domain_id="id2", escalations=[{"escalation_type": "google_safe", "id": "e2"}]), + ] + mock_dt_api.iris_detect_watched_domains.return_value = iter(domains) + + result = connector._iris_detect_get_escalated_domains({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_status() is True + assert ar.get_summary()["domain_count"] == 2 + assert len(ar.get_data()) == 2 + + def test_returns_success_with_no_results(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + result = connector._iris_detect_get_escalated_domains({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_summary()["domain_count"] == 0 + + def test_always_filters_google_safe(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + connector._iris_detect_get_escalated_domains({}) + + _, kwargs = mock_dt_api.iris_detect_watched_domains.call_args + assert kwargs["escalation_types"] == ["google_safe"] + + def test_passes_additional_params(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + connector._iris_detect_get_escalated_domains({ + "monitor_id": "mon789", + "tlds": "com", + "risk_score_ranges": "70-99", + "mx_exists": True, + "discovered_since": "2026-01-01T00:00:00Z", + "changed_since": "2026-01-02T00:00:00Z", + "escalated_since": "2026-01-03T00:00:00Z", + "search": "brand", + "sort": "changed_date", + "order": "desc", + "include_domain_data": True, + "limit": 20, + "preview": False, + }) + + mock_dt_api.iris_detect_watched_domains.assert_called_once_with( + monitor_id="mon789", + escalation_types=["google_safe"], + tlds="com", + risk_score_ranges="70-99", + mx_exists=True, + discovered_since="2026-01-01T00:00:00Z", + changed_since="2026-01-02T00:00:00Z", + escalated_since="2026-01-03T00:00:00Z", + search="brand", + sort="changed_date", + order="desc", + include_domain_data=True, + limit=20, + preview=False, + ) + + def test_google_safe_not_overridable_by_caller(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + # caller passes nothing — escalation_types must still be ["google_safe"] + connector._iris_detect_get_escalated_domains({"monitor_id": "mon1"}) + + _, kwargs = mock_dt_api.iris_detect_watched_domains.call_args + assert kwargs["escalation_types"] == ["google_safe"] + + def test_domain_data_stored_correctly(self, connector, mock_dt_api): + domain = make_domain( + "escalated.com", + state="watched", + risk_score=95, + domain_id="esc1", + escalations=[{"escalation_type": "google_safe", "id": "e99", "created": "2026-01-01T00:00:00Z"}], + ) + mock_dt_api.iris_detect_watched_domains.return_value = iter([domain]) + + connector._iris_detect_get_escalated_domains({}) + ar = connector.last_action_result() + + stored = ar.get_data()[0] + assert stored["domain"] == "escalated.com" + assert stored["risk_score"] == 95 + assert stored["escalations"][0]["escalation_type"] == "google_safe" + + def test_returns_error_on_api_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.side_effect = Exception("service error") + + result = connector._iris_detect_get_escalated_domains({}) + ar = connector.last_action_result() + + assert result is False + assert "service error" in ar.get_message() + + def test_returns_error_with_code_on_structured_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.side_effect = Exception(403, "Forbidden") + + connector._iris_detect_get_escalated_domains({}) + ar = connector.last_action_result() + + assert "403" in ar.get_message() + assert "Forbidden" in ar.get_message() diff --git a/tests/test_iris_detect_get_ignored_domains.py b/tests/test_iris_detect_get_ignored_domains.py new file mode 100644 index 0000000..2c3f9c8 --- /dev/null +++ b/tests/test_iris_detect_get_ignored_domains.py @@ -0,0 +1,110 @@ +"""Unit tests for _iris_detect_get_ignored_domains action.""" + +from tests.conftest import make_domain + + +class TestIrisDetectGetIgnoredDomains: + + def test_returns_success_with_results(self, connector, mock_dt_api): + domains = [make_domain("fp1.com", state="ignored", domain_id="id1"), make_domain("fp2.net", state="ignored", domain_id="id2")] + mock_dt_api.iris_detect_ignored_domains.return_value = iter(domains) + + result = connector._iris_detect_get_ignored_domains({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_status() is True + assert ar.get_summary()["domain_count"] == 2 + assert len(ar.get_data()) == 2 + + def test_returns_success_with_no_results(self, connector, mock_dt_api): + mock_dt_api.iris_detect_ignored_domains.return_value = iter([]) + + result = connector._iris_detect_get_ignored_domains({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_summary()["domain_count"] == 0 + assert ar.get_data() == [] + + def test_passes_all_params(self, connector, mock_dt_api): + mock_dt_api.iris_detect_ignored_domains.return_value = iter([]) + + connector._iris_detect_get_ignored_domains({ + "monitor_id": "mon456", + "tlds": "org", + "risk_score_ranges": "1-39", + "mx_exists": False, + "discovered_since": "2026-01-01T00:00:00Z", + "changed_since": "2026-01-02T00:00:00Z", + "escalated_since": "2026-01-03T00:00:00Z", + "search": "false-positive", + "sort": "discovered_date", + "order": "asc", + "include_domain_data": False, + "limit": 10, + "preview": True, + }) + + mock_dt_api.iris_detect_ignored_domains.assert_called_once_with( + monitor_id="mon456", + tlds="org", + risk_score_ranges="1-39", + mx_exists=False, + discovered_since="2026-01-01T00:00:00Z", + changed_since="2026-01-02T00:00:00Z", + escalated_since="2026-01-03T00:00:00Z", + search="false-positive", + sort="discovered_date", + order="asc", + include_domain_data=False, + limit=10, + preview=True, + ) + + def test_passes_escalated_since(self, connector, mock_dt_api): + mock_dt_api.iris_detect_ignored_domains.return_value = iter([]) + + connector._iris_detect_get_ignored_domains({"escalated_since": "2026-05-01T00:00:00Z"}) + + _, kwargs = mock_dt_api.iris_detect_ignored_domains.call_args + assert kwargs["escalated_since"] == "2026-05-01T00:00:00Z" + + def test_defaults_include_domain_data_to_false(self, connector, mock_dt_api): + mock_dt_api.iris_detect_ignored_domains.return_value = iter([]) + + connector._iris_detect_get_ignored_domains({}) + + _, kwargs = mock_dt_api.iris_detect_ignored_domains.call_args + assert kwargs["include_domain_data"] is False + + def test_domain_data_stored_correctly(self, connector, mock_dt_api): + domain = make_domain("ignored.com", state="ignored", risk_score=15, domain_id="ig1", tld="com") + mock_dt_api.iris_detect_ignored_domains.return_value = iter([domain]) + + connector._iris_detect_get_ignored_domains({}) + ar = connector.last_action_result() + + stored = ar.get_data()[0] + assert stored["domain"] == "ignored.com" + assert stored["state"] == "ignored" + assert stored["risk_score"] == 15 + assert stored["id"] == "ig1" + + def test_returns_error_on_api_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_ignored_domains.side_effect = Exception("connection timeout") + + result = connector._iris_detect_get_ignored_domains({}) + ar = connector.last_action_result() + + assert result is False + assert "connection timeout" in ar.get_message() + + def test_returns_error_with_code_on_structured_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_ignored_domains.side_effect = Exception(401, "Unauthorized") + + connector._iris_detect_get_ignored_domains({}) + ar = connector.last_action_result() + + assert "401" in ar.get_message() + assert "Unauthorized" in ar.get_message() diff --git a/tests/test_iris_detect_get_monitors_list.py b/tests/test_iris_detect_get_monitors_list.py new file mode 100644 index 0000000..d3974fb --- /dev/null +++ b/tests/test_iris_detect_get_monitors_list.py @@ -0,0 +1,122 @@ +"""Unit tests for _iris_detect_get_monitors_list action.""" + +from tests.conftest import make_monitor + + +class TestIrisDetectGetMonitorsList: + + def test_returns_success_with_results(self, connector, mock_dt_api): + monitors = [make_monitor("domaintools", "mon1"), make_monitor("acme", "mon2")] + mock_dt_api.iris_detect_monitors.return_value = iter(monitors) + + result = connector._iris_detect_get_monitors_list({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_status() is True + assert ar.get_summary()["monitor_count"] == 2 + assert len(ar.get_data()) == 2 + + def test_returns_success_with_no_results(self, connector, mock_dt_api): + mock_dt_api.iris_detect_monitors.return_value = iter([]) + + result = connector._iris_detect_get_monitors_list({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_summary()["monitor_count"] == 0 + assert ar.get_data() == [] + + def test_passes_sort_and_order(self, connector, mock_dt_api): + mock_dt_api.iris_detect_monitors.return_value = iter([]) + + connector._iris_detect_get_monitors_list({"sort": "created_date", "order": "asc"}) + + mock_dt_api.iris_detect_monitors.assert_called_once_with( + sort="created_date", + order="asc", + limit=None, + ) + + def test_default_order_is_desc(self, connector, mock_dt_api): + mock_dt_api.iris_detect_monitors.return_value = iter([]) + + connector._iris_detect_get_monitors_list({}) + + _, kwargs = mock_dt_api.iris_detect_monitors.call_args + assert kwargs["order"] == "desc" + + def test_passes_limit(self, connector, mock_dt_api): + mock_dt_api.iris_detect_monitors.return_value = iter([]) + + connector._iris_detect_get_monitors_list({"limit": 100}) + + _, kwargs = mock_dt_api.iris_detect_monitors.call_args + assert kwargs["limit"] == 100 + + def test_passes_include_counts_with_datetime(self, connector, mock_dt_api): + mock_dt_api.iris_detect_monitors.return_value = iter([]) + + connector._iris_detect_get_monitors_list({ + "include_counts": True, + "datetime_counts_since": "2026-01-01T00:00:00Z", + }) + + mock_dt_api.iris_detect_monitors.assert_called_once_with( + sort=None, + order="desc", + limit=None, + include_counts=True, + datetime_counts_since="2026-01-01T00:00:00Z", + ) + + def test_include_counts_false_does_not_pass_datetime(self, connector, mock_dt_api): + mock_dt_api.iris_detect_monitors.return_value = iter([]) + + connector._iris_detect_get_monitors_list({"include_counts": False}) + + _, kwargs = mock_dt_api.iris_detect_monitors.call_args + assert "include_counts" not in kwargs + assert "datetime_counts_since" not in kwargs + + def test_monitor_data_stored_correctly(self, connector, mock_dt_api): + monitor = make_monitor("mybrand", "mon42", state="active", status="completed", created_by="admin") + mock_dt_api.iris_detect_monitors.return_value = iter([monitor]) + + connector._iris_detect_get_monitors_list({}) + ar = connector.last_action_result() + + stored = ar.get_data()[0] + assert stored["term"] == "mybrand" + assert stored["id"] == "mon42" + assert stored["state"] == "active" + assert stored["status"] == "completed" + assert stored["created_by"] == "admin" + + def test_summary_key_is_monitor_count(self, connector, mock_dt_api): + monitors = [make_monitor("a", "m1"), make_monitor("b", "m2"), make_monitor("c", "m3")] + mock_dt_api.iris_detect_monitors.return_value = iter(monitors) + + connector._iris_detect_get_monitors_list({}) + ar = connector.last_action_result() + + assert "monitor_count" in ar.get_summary() + assert ar.get_summary()["monitor_count"] == 3 + + def test_returns_error_on_api_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_monitors.side_effect = Exception("API error") + + result = connector._iris_detect_get_monitors_list({}) + ar = connector.last_action_result() + + assert result is False + assert "API error" in ar.get_message() + + def test_returns_error_with_code_on_structured_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_monitors.side_effect = Exception(403, "Forbidden") + + connector._iris_detect_get_monitors_list({}) + ar = connector.last_action_result() + + assert "403" in ar.get_message() + assert "Forbidden" in ar.get_message() diff --git a/tests/test_iris_detect_get_new_domains.py b/tests/test_iris_detect_get_new_domains.py new file mode 100644 index 0000000..d9e1790 --- /dev/null +++ b/tests/test_iris_detect_get_new_domains.py @@ -0,0 +1,128 @@ +"""Unit tests for _iris_detect_get_new_domains action.""" + +from tests.conftest import make_domain + + +class TestIrisDetectGetNewDomains: + + def test_returns_success_with_results(self, connector, mock_dt_api): + domains = [make_domain("evil.com", domain_id="id1"), make_domain("phish.net", domain_id="id2")] + mock_dt_api.iris_detect_new_domains.return_value = iter(domains) + + result = connector._iris_detect_get_new_domains({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_status() is True + assert ar.get_summary()["domain_count"] == 2 + assert len(ar.get_data()) == 2 + + def test_returns_success_with_no_results(self, connector, mock_dt_api): + mock_dt_api.iris_detect_new_domains.return_value = iter([]) + + result = connector._iris_detect_get_new_domains({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_summary()["domain_count"] == 0 + assert ar.get_data() == [] + + def test_passes_monitor_id(self, connector, mock_dt_api): + mock_dt_api.iris_detect_new_domains.return_value = iter([]) + + connector._iris_detect_get_new_domains({"monitor_id": "mon123"}) + + mock_dt_api.iris_detect_new_domains.assert_called_once_with( + monitor_id="mon123", + tlds=None, + risk_score_ranges=None, + mx_exists=None, + discovered_since=None, + changed_since=None, + search=None, + sort=None, + order=None, + include_domain_data=False, + limit=None, + preview=None, + ) + + def test_passes_discovered_since(self, connector, mock_dt_api): + mock_dt_api.iris_detect_new_domains.return_value = iter([]) + + connector._iris_detect_get_new_domains({"discovered_since": "2026-01-01T00:00:00Z"}) + + _, kwargs = mock_dt_api.iris_detect_new_domains.call_args + assert kwargs["discovered_since"] == "2026-01-01T00:00:00Z" + + def test_passes_risk_score_ranges(self, connector, mock_dt_api): + mock_dt_api.iris_detect_new_domains.return_value = iter([]) + + connector._iris_detect_get_new_domains({"risk_score_ranges": "70-99,100-100"}) + + _, kwargs = mock_dt_api.iris_detect_new_domains.call_args + assert kwargs["risk_score_ranges"] == "70-99,100-100" + + def test_passes_include_domain_data(self, connector, mock_dt_api): + mock_dt_api.iris_detect_new_domains.return_value = iter([]) + + connector._iris_detect_get_new_domains({"include_domain_data": True}) + + _, kwargs = mock_dt_api.iris_detect_new_domains.call_args + assert kwargs["include_domain_data"] is True + + def test_passes_preview(self, connector, mock_dt_api): + mock_dt_api.iris_detect_new_domains.return_value = iter([]) + + connector._iris_detect_get_new_domains({"preview": True}) + + _, kwargs = mock_dt_api.iris_detect_new_domains.call_args + assert kwargs["preview"] is True + + def test_passes_limit(self, connector, mock_dt_api): + mock_dt_api.iris_detect_new_domains.return_value = iter([]) + + connector._iris_detect_get_new_domains({"limit": 50}) + + _, kwargs = mock_dt_api.iris_detect_new_domains.call_args + assert kwargs["limit"] == 50 + + def test_passes_sort_and_order(self, connector, mock_dt_api): + mock_dt_api.iris_detect_new_domains.return_value = iter([]) + + connector._iris_detect_get_new_domains({"sort": "risk_score", "order": "desc"}) + + _, kwargs = mock_dt_api.iris_detect_new_domains.call_args + assert kwargs["sort"] == "risk_score" + assert kwargs["order"] == "desc" + + def test_domain_data_stored_correctly(self, connector, mock_dt_api): + domain = make_domain("evil.com", risk_score=99, domain_id="xyz", state="new") + mock_dt_api.iris_detect_new_domains.return_value = iter([domain]) + + connector._iris_detect_get_new_domains({}) + ar = connector.last_action_result() + + stored = ar.get_data()[0] + assert stored["domain"] == "evil.com" + assert stored["risk_score"] == 99 + assert stored["id"] == "xyz" + assert stored["state"] == "new" + + def test_returns_error_on_api_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_new_domains.side_effect = Exception("API unavailable") + + result = connector._iris_detect_get_new_domains({}) + ar = connector.last_action_result() + + assert result is False + assert "API unavailable" in ar.get_message() + + def test_returns_error_with_code_on_structured_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_new_domains.side_effect = Exception(401, "Unauthorized") + + connector._iris_detect_get_new_domains({}) + ar = connector.last_action_result() + + assert "401" in ar.get_message() + assert "Unauthorized" in ar.get_message() diff --git a/tests/test_iris_detect_get_watched_domains.py b/tests/test_iris_detect_get_watched_domains.py new file mode 100644 index 0000000..7cd737e --- /dev/null +++ b/tests/test_iris_detect_get_watched_domains.py @@ -0,0 +1,116 @@ +"""Unit tests for _iris_detect_get_watched_domains action.""" + +from tests.conftest import make_domain + + +class TestIrisDetectGetWatchedDomains: + + def test_returns_success_with_results(self, connector, mock_dt_api): + domains = [make_domain("evil.com", state="watched", domain_id="id1"), make_domain("phish.net", state="watched", domain_id="id2")] + mock_dt_api.iris_detect_watched_domains.return_value = iter(domains) + + result = connector._iris_detect_get_watched_domains({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_status() is True + assert ar.get_summary()["domain_count"] == 2 + assert len(ar.get_data()) == 2 + + def test_returns_success_with_no_results(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + result = connector._iris_detect_get_watched_domains({}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_summary()["domain_count"] == 0 + assert ar.get_data() == [] + + def test_passes_all_params(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + connector._iris_detect_get_watched_domains({ + "monitor_id": "mon123", + "tlds": "com,net", + "risk_score_ranges": "70-99", + "mx_exists": True, + "discovered_since": "2026-01-01T00:00:00Z", + "changed_since": "2026-01-02T00:00:00Z", + "escalated_since": "2026-01-03T00:00:00Z", + "search": "evil", + "sort": "risk_score", + "order": "desc", + "include_domain_data": True, + "limit": 25, + "preview": False, + }) + + mock_dt_api.iris_detect_watched_domains.assert_called_once_with( + monitor_id="mon123", + tlds="com,net", + risk_score_ranges="70-99", + mx_exists=True, + discovered_since="2026-01-01T00:00:00Z", + changed_since="2026-01-02T00:00:00Z", + escalated_since="2026-01-03T00:00:00Z", + search="evil", + sort="risk_score", + order="desc", + include_domain_data=True, + limit=25, + preview=False, + ) + + def test_passes_escalated_since(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + connector._iris_detect_get_watched_domains({"escalated_since": "2026-06-01T00:00:00Z"}) + + _, kwargs = mock_dt_api.iris_detect_watched_domains.call_args + assert kwargs["escalated_since"] == "2026-06-01T00:00:00Z" + + def test_defaults_include_domain_data_to_false(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.return_value = iter([]) + + connector._iris_detect_get_watched_domains({}) + + _, kwargs = mock_dt_api.iris_detect_watched_domains.call_args + assert kwargs["include_domain_data"] is False + + def test_domain_data_stored_correctly(self, connector, mock_dt_api): + domain = make_domain( + "watched.com", + state="watched", + risk_score=72, + domain_id="w1", + escalations=[{"escalation_type": "blocked", "id": "esc1"}], + ) + mock_dt_api.iris_detect_watched_domains.return_value = iter([domain]) + + connector._iris_detect_get_watched_domains({}) + ar = connector.last_action_result() + + stored = ar.get_data()[0] + assert stored["domain"] == "watched.com" + assert stored["state"] == "watched" + assert stored["risk_score"] == 72 + assert stored["escalations"][0]["escalation_type"] == "blocked" + + def test_returns_error_on_api_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.side_effect = Exception("API unavailable") + + result = connector._iris_detect_get_watched_domains({}) + ar = connector.last_action_result() + + assert result is False + assert "API unavailable" in ar.get_message() + + def test_returns_error_with_code_on_structured_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_watched_domains.side_effect = Exception(403, "Forbidden") + + connector._iris_detect_get_watched_domains({}) + ar = connector.last_action_result() + + assert "403" in ar.get_message() + assert "Forbidden" in ar.get_message() diff --git a/tests/test_iris_detect_ignore_domains.py b/tests/test_iris_detect_ignore_domains.py new file mode 100644 index 0000000..ec0fa19 --- /dev/null +++ b/tests/test_iris_detect_ignore_domains.py @@ -0,0 +1,108 @@ +"""Unit tests for _iris_detect_ignore_domains action.""" + +from tests.conftest import make_domain + + +class TestIrisDetectIgnoreDomains: + + def test_returns_success_with_results(self, connector, mock_dt_api): + domains = [make_domain("fp1.com", state="ignored", domain_id="id1"), make_domain("fp2.net", state="ignored", domain_id="id2")] + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter(domains) + + result = connector._iris_detect_ignore_domains({"watchlist_domain_ids": "id1,id2"}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_status() is True + assert ar.get_summary()["ignored_count"] == 2 + assert len(ar.get_data()) == 2 + + def test_returns_success_with_single_id(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([make_domain("fp1.com", state="ignored", domain_id="id1")]) + + result = connector._iris_detect_ignore_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_summary()["ignored_count"] == 1 + + def test_always_uses_ignored_state(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([]) + + connector._iris_detect_ignore_domains({"watchlist_domain_ids": "id1"}) + + _, kwargs = mock_dt_api.iris_detect_manage_watchlist_domains.call_args + assert kwargs["state"] == "ignored" + + def test_state_is_not_watched(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([]) + + connector._iris_detect_ignore_domains({"watchlist_domain_ids": "id1"}) + + _, kwargs = mock_dt_api.iris_detect_manage_watchlist_domains.call_args + assert kwargs["state"] != "watched" + + def test_passes_single_domain_id_as_list(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([]) + + connector._iris_detect_ignore_domains({"watchlist_domain_ids": "abc123"}) + + _, kwargs = mock_dt_api.iris_detect_manage_watchlist_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["abc123"] + + def test_passes_multiple_domain_ids_as_list(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([]) + + connector._iris_detect_ignore_domains({"watchlist_domain_ids": "id1,id2,id3"}) + + _, kwargs = mock_dt_api.iris_detect_manage_watchlist_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["id1", "id2", "id3"] + + def test_strips_spaces_from_domain_ids(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([]) + + connector._iris_detect_ignore_domains({"watchlist_domain_ids": "id1, id2, id3"}) + + _, kwargs = mock_dt_api.iris_detect_manage_watchlist_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["id1", "id2", "id3"] + + def test_domain_data_stored_correctly(self, connector, mock_dt_api): + domain = make_domain("ignored.com", state="ignored", domain_id="ig99", discovered_date="2026-01-01T00:00:00Z") + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([domain]) + + connector._iris_detect_ignore_domains({"watchlist_domain_ids": "ig99"}) + ar = connector.last_action_result() + + stored = ar.get_data()[0] + assert stored["domain"] == "ignored.com" + assert stored["state"] == "ignored" + assert stored["id"] == "ig99" + assert stored["discovered_date"] == "2026-01-01T00:00:00Z" + + def test_summary_key_is_ignored_count(self, connector, mock_dt_api): + domains = [make_domain(f"fp{i}.com", state="ignored", domain_id=f"id{i}") for i in range(3)] + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter(domains) + + connector._iris_detect_ignore_domains({"watchlist_domain_ids": "id0,id1,id2"}) + ar = connector.last_action_result() + + assert "ignored_count" in ar.get_summary() + assert ar.get_summary()["ignored_count"] == 3 + + def test_returns_error_on_api_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.side_effect = Exception("connection refused") + + result = connector._iris_detect_ignore_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert result is False + assert "connection refused" in ar.get_message() + + def test_returns_error_with_code_on_structured_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.side_effect = Exception(401, "Unauthorized") + + connector._iris_detect_ignore_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert "401" in ar.get_message() + assert "Unauthorized" in ar.get_message() diff --git a/tests/test_iris_detect_watch_domains.py b/tests/test_iris_detect_watch_domains.py new file mode 100644 index 0000000..967032f --- /dev/null +++ b/tests/test_iris_detect_watch_domains.py @@ -0,0 +1,108 @@ +"""Unit tests for _iris_detect_watch_domains action.""" + +from tests.conftest import make_domain + + +class TestIrisDetectWatchDomains: + + def test_returns_success_with_results(self, connector, mock_dt_api): + domains = [make_domain("watch1.com", state="watched", domain_id="id1"), make_domain("watch2.net", state="watched", domain_id="id2")] + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter(domains) + + result = connector._iris_detect_watch_domains({"watchlist_domain_ids": "id1,id2"}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_status() is True + assert ar.get_summary()["watched_count"] == 2 + assert len(ar.get_data()) == 2 + + def test_returns_success_with_single_id(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([make_domain("watch1.com", state="watched", domain_id="id1")]) + + result = connector._iris_detect_watch_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert result is True + assert ar.get_summary()["watched_count"] == 1 + + def test_always_uses_watched_state(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([]) + + connector._iris_detect_watch_domains({"watchlist_domain_ids": "id1"}) + + _, kwargs = mock_dt_api.iris_detect_manage_watchlist_domains.call_args + assert kwargs["state"] == "watched" + + def test_state_is_not_ignored(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([]) + + connector._iris_detect_watch_domains({"watchlist_domain_ids": "id1"}) + + _, kwargs = mock_dt_api.iris_detect_manage_watchlist_domains.call_args + assert kwargs["state"] != "ignored" + + def test_passes_single_domain_id_as_list(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([]) + + connector._iris_detect_watch_domains({"watchlist_domain_ids": "abc123"}) + + _, kwargs = mock_dt_api.iris_detect_manage_watchlist_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["abc123"] + + def test_passes_multiple_domain_ids_as_list(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([]) + + connector._iris_detect_watch_domains({"watchlist_domain_ids": "id1,id2,id3"}) + + _, kwargs = mock_dt_api.iris_detect_manage_watchlist_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["id1", "id2", "id3"] + + def test_strips_spaces_from_domain_ids(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([]) + + connector._iris_detect_watch_domains({"watchlist_domain_ids": "id1, id2, id3"}) + + _, kwargs = mock_dt_api.iris_detect_manage_watchlist_domains.call_args + assert kwargs["watchlist_domain_ids"] == ["id1", "id2", "id3"] + + def test_domain_data_stored_correctly(self, connector, mock_dt_api): + domain = make_domain("watched.com", state="watched", domain_id="w99", discovered_date="2026-01-01T00:00:00Z") + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter([domain]) + + connector._iris_detect_watch_domains({"watchlist_domain_ids": "w99"}) + ar = connector.last_action_result() + + stored = ar.get_data()[0] + assert stored["domain"] == "watched.com" + assert stored["state"] == "watched" + assert stored["id"] == "w99" + assert stored["discovered_date"] == "2026-01-01T00:00:00Z" + + def test_summary_key_is_watched_count(self, connector, mock_dt_api): + domains = [make_domain(f"d{i}.com", state="watched", domain_id=f"id{i}") for i in range(5)] + mock_dt_api.iris_detect_manage_watchlist_domains.return_value = iter(domains) + + connector._iris_detect_watch_domains({"watchlist_domain_ids": "id0,id1,id2,id3,id4"}) + ar = connector.last_action_result() + + assert "watched_count" in ar.get_summary() + assert ar.get_summary()["watched_count"] == 5 + + def test_returns_error_on_api_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.side_effect = Exception("service unavailable") + + result = connector._iris_detect_watch_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert result is False + assert "service unavailable" in ar.get_message() + + def test_returns_error_with_code_on_structured_exception(self, connector, mock_dt_api): + mock_dt_api.iris_detect_manage_watchlist_domains.side_effect = Exception(403, "Forbidden") + + connector._iris_detect_watch_domains({"watchlist_domain_ids": "id1"}) + ar = connector.last_action_result() + + assert "403" in ar.get_message() + assert "Forbidden" in ar.get_message()