IPQualityScore

Proactively Prevent Fraud.

Data Enrichment & Threat Intelligence · IPQualityScore (IPQS) Threat Risk Scoring

Details

IDIPQualityScore
ProviderIPQualityScore
CategoryData Enrichment & Threat Intelligence
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10230415
Supported ModulesAgentix XSIAM

README

Overview

IPQualityScore (IPQS) provides enterprise-grade real-time threat intelligence for IP addresses, email addresses, URLs, phone numbers, and files. Detect fraud, phishing, malware, leaked credentials, and abusive behavior using IPQS’s global threat network.

This integration was integrated and tested with version 1.0 of IPQualityScore.

Getting an API Key

  1. Register for a free account at https://www.ipqualityscore.com/create-account. Free accounts include 5,000 API lookups per month.
  2. After registering, log in and visit https://www.ipqualityscore.com/documentation/proxy-detection/overview. Your API key is listed under the Private Key subheading.

Configure IPQualityScore in Cortex

Parameter Description Required
API Key Your IPQS private API key. True
Trust any certificate (not secure) Skip SSL certificate verification. False
Use system proxy settings Route requests through the system proxy. False
IP Suspicious Score Threshold Threshold for fraud score from IP lookup to be marked as suspicious. False
IP Malicious Score Threshold Threshold for fraud score from IP lookup to be marked as malicious. False
Email Suspicious Score Threshold Threshold for fraud score from Email lookup to be marked as suspicious. False
Email Malicious Score Threshold Threshold for fraud score from Email lookup to be marked as malicious. False
Url Suspicious Score Threshold Threshold for fraud score from Url lookup to be marked as suspicious. False
Url Malicious Score Threshold Threshold for fraud score from Url lookup to be marked as malicious. False
Phone Suspicious Score Threshold Threshold for fraud score from Phone lookup to be marked as suspicious. False
Phone Malicious Score Threshold Threshold for fraud score from Phone lookup to be marked as malicious. False
Source Reliability Reliability of the source providing the intelligence data. True

Commands

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

ipqs-ip-reputation


Runs reputation on IPs.

Base Command

ipqs-ip-reputation

Input

Argument Name Description Required
ip IP address(es) to check. Supports comma-separated list. Required

Context Output

Path Type Description
IPQualityScore.IP.success Boolean Was the request successful?
IPQualityScore.IP.message String A generic status message, either success or some form of an error notice.
IPQualityScore.IP.fraud_score Number The overall fraud score of the user based on the IP, user agent, language, and any other optionally passed variables. Fraud Scores >= 75 are suspicious, but not necessarily fraudulent. We recommend flagging or blocking traffic with Fraud Scores >= 85, but you may find it beneficial to use a higher or lower threshold.
IPQualityScore.IP.country_code String Two character country code of IP address or “N/A” if unknown.
IPQualityScore.IP.region String Region (state) of IP address if available or “N/A” if unknown.
IPQualityScore.IP.city String City of IP address if available or “N/A” if unknown.
IPQualityScore.IP.ISP String ISP if one is known. Otherwise “N/A”.
IPQualityScore.IP.ASN Number Autonomous System Number if one is known. Null if nonexistent.
IPQualityScore.IP.organization String Organization if one is known. Can be parent company or sub company of the listed ISP. Otherwise “N/A”.
IPQualityScore.IP.latitude Number Latitude of IP address if available or “N/A” if unknown.
IPQualityScore.IP.longitude Number Longitude of IP address if available or “N/A” if unknown.
IPQualityScore.IP.is_crawler Boolean Is this IP associated with being a confirmed crawler from a mainstream search engine such as Googlebot, Bingbot, Yandex, etc. based on hostname or IP address verification.
IPQualityScore.IP.timezone String Timezone of IP address if available or “N/A” if unknown.
IPQualityScore.IP.mobile Boolean Is this user agent a mobile browser? (will always be false if the user agent is not passed in the API request)
IPQualityScore.IP.host String Hostname of the IP address if one is available.
IPQualityScore.IP.proxy Boolean Is this IP address suspected to be a proxy? (SOCKS, Elite, Anonymous, VPN, Tor, etc.)
IPQualityScore.IP.vpn Boolean Is this IP suspected of being a VPN connection? This can include data center ranges which can become active VPNs at any time. The “proxy” status will always be true when this value is true.
IPQualityScore.IP.tor Boolean Is this IP suspected of being a TOR connection? This can include previously active TOR nodes and exits which can become active TOR exits at any time. The “proxy” status will always be true when this value is true.
IPQualityScore.IP.active_vpn Boolean Premium Account Feature - Identifies active VPN connections used by popular VPN services and private VPN servers.
IPQualityScore.IP.active_tor Boolean Premium Account Feature - Identifies active TOR exits on the TOR network.
IPQualityScore.IP.recent_abuse Boolean This value will indicate if there has been any recently verified abuse across our network for this IP address. Abuse could be a confirmed chargeback, compromised device, fake app install, or similar malicious behavior within the past few days.
IPQualityScore.IP.bot_status Boolean Premium Account Feature - Indicates if bots or non-human traffic has recently used this IP address to engage in automated fraudulent behavior. Provides stronger confidence that the IP address is suspicious.
IPQualityScore.IP.connection_type String Classification of the IP address connection type as “Residential”, “Corporate”, “Education”, “Mobile”, or “Data Center”.
IPQualityScore.IP.abuse_velocity String Premium Account Feature - How frequently the IP address is engaging in abuse across the IPQS threat network. Values can be “high”, “medium”, “low”, or “none”. Can be used in combination with the Fraud Score to identify bad behavior.
IPQualityScore.IP.request_id String A unique identifier for this request that can be used to lookup the request details or send a postback conversion notice.
IPQualityScore.IP.address String The IP address that was queried.
DBotScore.Indicator String The indicator that was tested.
DBotScore.Score Number The actual score.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.
IP.Address string IP address
IP.ASN string The autonomous system name for the IP address, for example: “AS8948”.
IP.Hostname string The hostname that is mapped to this IP address.
IP.Geo.Country string The country in which the IP address is located.
IP.Geo.Description string Additional information about the location.
IP.Malicious.Vendor string The vendor reporting the IP address as malicious.
IP.Malicious.Description string A description explaining why the IP address was reported as malicious.

Command Example

!ipqs-ip-reputation ip="8.8.8.8"

Context Example

{
    "DBotScore": {
        "Indicator": "8.8.8.8",
        "Reliability": "A - Completely reliable",
        "Score": 3,
        "Type": "ip",
        "Vendor": "IPQualityScore"
    },
    "IP": {
        "ASN": 15169,
        "Address": "8.8.8.8",
        "Geo": {
            "Country": "US",
            "Location": "37.39:-122.07"
        },
        "Hostname": "dns.google",
        "Malicious": {
            "Description": null,
            "Vendor": "IPQualityScore"
        }
    },
    "IPQualityScore": {
        "IP": {
            "ASN": 15169,
            "ISP": "Google",
            "Malicious": {
                "Vendor": "IPQualityScore"
            },
            "abuse_velocity": "high",
            "active_tor": false,
            "active_vpn": false,
            "address": "8.8.8.8",
            "bot_status": true,
            "city": "Mountain View",
            "connection_type": "Data Center",
            "country_code": "US",
            "fraud_score": 100,
            "host": "dns.google",
            "is_crawler": false,
            "latitude": 37.39,
            "longitude": -122.07,
            "message": "Success",
            "mobile": false,
            "organization": "Google",
            "proxy": true,
            "recent_abuse": true,
            "region": "California",
            "request_id": "4DpK9WpOZGPFSPg",
            "success": true,
            "timezone": "America/Los_Angeles",
            "tor": false,
            "vpn": true
        }
    }
}

Human Readable Output

IPQualityScore Results for 8.8.8.8

success message fraud_score country_code region city ISP ASN organization latitude longitude is_crawler timezone mobile host proxy vpn tor active_vpn active_tor recent_abuse bot_status connection_type abuse_velocity request_id address
true Success 100 US California Mountain View Google 15169 Google 37.39 -122.07 false America/Los_Angeles false dns.google true true false false false true true Data Center high 4DpK9WpOZGPFSPg 8.8.8.8

ip


Deprecated. Use ipqs-ip-reputation instead.

Base Command

ip

Input

Argument Name Description Required
ip IP address(es) to check. Supports comma-separated list. Required

Command Example

!ip ip="8.8.8.8"


ipqs-email-reputation


Runs reputation on email addresses.

Base Command

ipqs-email-reputation

Input

Argument Name Description Required
email Email address(es) to check. Supports comma-separated list. Required

Context Output

Path Type Description
IPQualityScore.Email.message String A generic status message, either success or some form of an error notice.
IPQualityScore.Email.success Boolean Was the request successful?
IPQualityScore.Email.valid Boolean Does this email address appear valid?
IPQualityScore.Email.disposable Boolean Is this email suspected of belonging to a temporary or disposable mail service? Usually associated with fraudsters and scammers.
IPQualityScore.Email.smtp_score Number Validity score of email server’s SMTP setup. Range: “-1” - “3”. Scores above “-1” can be associated with a valid email. -1 = invalid email address 0 = mail server exists, but is rejecting all mail 1 = mail server exists, but is showing a temporary error 2 = mail server exists, but accepts all email 3 = mail server exists and has verified the email address
IPQualityScore.Email.overall_score Number Overall email validity score. Range: “0” - “4”. Scores above “1” can be associated with a valid email. 0 = invalid email address 1 = dns valid, unreachable mail server 2 = dns valid, temporary mail rejection error 3 = dns valid, accepts all mail 4 = dns valid, verified email exists
IPQualityScore.Email.first_name String Suspected first name based on email. Returns “CORPORATE” if the email is suspected of being a generic company email. Returns “UNKNOWN” if the first name was not determinable.
IPQualityScore.Email.generic Boolean Is this email suspected as being a catch all or shared email for a domain? (“admin@”, “webmaster@”, “newsletter@”, “sales@”, “contact@”, etc.)
IPQualityScore.Email.common Boolean Is this email from a common email provider? (“gmail.com”, “yahoo.com”, “hotmail.com”, etc.)
IPQualityScore.Email.dns_valid Boolean Does the email’s hostname have valid DNS entries? Partial indication of a valid email.
IPQualityScore.Email.honeypot Boolean Is this email believed to be a “honeypot” or “SPAM trap”? Bulk mail sent to these emails increases your risk of being added to block lists by large ISPs & ending up in the spam folder.
IPQualityScore.Email.deliverability String How likely is this email to be delivered to the user and land in their mailbox. Values can be “high”, “medium”, or “low”.
IPQualityScore.Email.frequent_complainer Boolean Indicates if this email frequently unsubscribes from marketing lists or reports email as SPAM.
IPQualityScore.Email.spam_trap_score String Confidence level of the email address being an active SPAM trap. Values can be “high”, “medium”, “low”, or “none”. We recommend scrubbing emails with “high” or “medium” statuses. Avoid “low” emails whenever possible for any promotional mailings.
IPQualityScore.Email.catch_all Boolean Is this email likely to be a “catch all” where the mail server verifies all emails tested against it as valid? It is difficult to determine if the address is truly valid in these scenarios, since the email’s server will not confirm the account’s status.
IPQualityScore.Email.timed_out Boolean Did the connection to the mail service provider timeout during the verification? If so, we recommend increasing the “timeout” variable above the default 7 second value. Lookups that timeout with a “valid” result as false are most likely false and should be not be trusted.
IPQualityScore.Email.suspect Boolean This value indicates if the mail server is currently replying with a temporary error and unable to verify the email address. This status will also be true for “catch all” email addresses as defined below. If this value is true, then we suspect the “valid” result may be tainted and there is not a guarantee that the email address is truly valid.
IPQualityScore.Email.recent_abuse Boolean This value will indicate if there has been any recently verified abuse across our network for this email address. Abuse could be a confirmed chargeback, fake signup, compromised device, fake app install, or similar malicious behavior within the past few days.
IPQualityScore.Email.fraud_score Number The overall Fraud Score of the user based on the email’s reputation and recent behavior across the IPQS threat network. Fraud Scores >= 75 are suspicious, but not necessarily fraudulent.
IPQualityScore.Email.suggested_domain String Default value is “N/A”. Indicates if this email’s domain should in fact be corrected to a popular mail service. This field is useful for catching user typos. For example, an email address with “gmai.com”, would display a suggested domain of “gmail.com”. This feature supports all major mail service providers.
IPQualityScore.Email.leaked Boolean Was this email address associated with a recent database leak from a third party? Leaked accounts pose a risk as they may have become compromised during a database breach.
IPQualityScore.Email.domain_age.human Date A human description of when this domain was registered. (Ex: 3 months ago)
IPQualityScore.Email.domain_age.timestamp Number The unix time since epoch when this domain was first registered. (Ex: 1568061634)
IPQualityScore.Email.domain_age.iso Date The time this domain was registered in ISO8601 format (Ex: 2019-09-09T16:40:34-04:00)
IPQualityScore.Email.first_seen.human Date A human description of the email address age, using an estimation of the email creation date when IPQS first discovered this email address. (Ex: 3 months ago)
IPQualityScore.Email.first_seen.timestamp Number The unix time since epoch when this email was first analyzed by IPQS. (Ex: 1568061634)
IPQualityScore.Email.first_seen.iso Date The time this email was first analyzed by IPQS in ISO8601 format (Ex: 2019-09-09T16:40:34-04:00)
IPQualityScore.Email.sanitized_email String Sanitized email address with all aliases and masking removed, such as multiple periods for Gmail.com.
IPQualityScore.Email.request_id String A unique identifier for this request that can be used to lookup the request details or send a postback conversion notice.
IPQualityScore.Email.address String The email address that was queried
DBotScore.Indicator String The indicator that was tested.
DBotScore.Score Number The actual score.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.

Command Example

!ipqs-email-reputation email="noreply@example.com"

Context Example

{
    "DBotScore": {
        "Indicator": "noreply@ipqualityscore.com",
        "Reliability": "A - Completely reliable",
        "Score": 3,
        "Type": "email",
        "Vendor": "IPQualityScore"
    },
    "Email": {
        "Address": "noreply@ipqualityscore.com",
        "Domain": "ipqualityscore.com"
    },
    "IPQualityScore": {
        "Email": {
            "Malicious": {
                "Vendor": "IPQualityScore"
            },
            "address": "noreply@ipqualityscore.com",
            "catch_all": true,
            "common": false,
            "deliverability": "low",
            "disposable": false,
            "dns_valid": true,
            "domain_age": {
                "human": "10 years ago",
                "iso": "2011-04-14T23:26:37-04:00",
                "timestamp": 1302837997
            },
            "first_name": "Corporate",
            "first_seen": {
                "human": "2 years ago",
                "iso": "2019-05-06T02:09:06-04:00",
                "timestamp": 1557122946
            },
            "fraud_score": 95,
            "frequent_complainer": false,
            "generic": true,
            "honeypot": true,
            "leaked": false,
            "message": "Success.",
            "overall_score": 3,
            "recent_abuse": true,
            "request_id": "4DpK9WpOZGQDnr7",
            "sanitized_email": "noreply@ipqualityscore.com",
            "smtp_score": 2,
            "spam_trap_score": "medium",
            "success": true,
            "suggested_domain": "N/A",
            "suspect": true,
            "timed_out": false,
            "valid": true
        }
    }
}

Human Readable Output

IPQualityScore Results for noreply@ipqualityscore.com

message success valid disposable smtp_score overall_score first_name generic common dns_valid honeypot deliverability frequent_complainer spam_trap_score catch_all timed_out suspect recent_abuse fraud_score suggested_domain leaked domain_age first_seen sanitized_email request_id address
Success. true true false 2 3 Corporate true false true true low false medium true false true true 95 N/A false human: 10 years ago
timestamp: 1302837997
iso: 2011-04-14T23:26:37-04:00
human: 2 years ago
timestamp: 1557122946
iso: 2019-05-06T02:09:06-04:00
noreply@ipqualityscore.com 4DpK9WpOZGQDnr7 noreply@ipqualityscore.com

email


Deprecated. Use ipqs-email-reputation instead.

Base Command

email

Input

Argument Name Description Required
email Email address(es) to check. Supports comma-separated list. Required

Command Example

!email email="noreply@example.com"


ipqs-url-reputation


Runs reputation on URLs.

Notice: Submitting indicators using this command might make the indicator data publicly available. See the vendor’s documentation for more details.

Base Command

ipqs-url-reputation

Input

Argument Name Description Required
url URL(s) to check. Supports comma-separated list. Required

Context Output

Path Type Description
IPQualityScore.Url.message String A generic status message, either success or some form of an error notice.
IPQualityScore.Url.success Boolean Was the request successful?
IPQualityScore.Url.unsafe Boolean Is this domain suspected of being unsafe due to phishing, malware, spamming, or abusive behavior? View the confidence level by analyzing the “risk_score”.
IPQualityScore.Url.domain String Domain name of the final destination URL of the scanned link, after following all redirects.
IPQualityScore.Url.ip_address String The IP address corresponding to the server of the domain name.
IPQualityScore.Url.server String The server banner of the domain’s IP address. For example: “nginx/1.16.0”. Value will be “N/A” if unavailable.
IPQualityScore.Url.content_type String MIME type of URL’s content. For example “text/html; charset=UTF-8”. Value will be “N/A” if unavailable.
IPQualityScore.Url.status_code Number HTTP Status Code of the URL’s response. This value should be “200” for a valid website. Value is “0” if URL is unreachable.
IPQualityScore.Url.page_size Number Total number of bytes to download the URL’s content. Value is “0” if URL is unreachable.
IPQualityScore.Url.domain_rank Number Estimated popularity rank of website globally. Value is “0” if the domain is unranked or has low traffic.
IPQualityScore.Url.dns_valid Boolean The domain of the URL has valid DNS records.
IPQualityScore.Url.parking Boolean Is the domain of this URL currently parked with a for sale notice?
IPQualityScore.Url.spamming Boolean Is the domain of this URL associated with email SPAM or abusive email addresses?
IPQualityScore.Url.malware Boolean Is this URL associated with malware or viruses?
IPQualityScore.Url.phishing Boolean Is this URL associated with malicious phishing behavior?
IPQualityScore.Url.suspicious Boolean Is this URL suspected of being malicious or used for phishing or abuse? Use in conjunction with the “risk_score” as a confidence level.
IPQualityScore.Url.risk_score Number The IPQS risk score which estimates the confidence level for malicious URL detection. Risk Scores 85+ are high risk, while Risk Scores = 100 are confirmed as accurate.
IPQualityScore.Url.request_id String A unique identifier for this request that can be used to lookup the request details or send a postback conversion notice.
IPQualityScore.Url.url String The URL being queried.
IPQualityScore.Url.adult Boolean Is this URL or domain hosting dating or adult content?
IPQualityScore.Url.domain_age.human Date A human description of when this domain was registered. (Ex: 3 months ago)
IPQualityScore.Url.domain_age.timestamp Number The unix time since epoch when this domain was first registered. (Ex: 1568061634)
IPQualityScore.Url.domain_age.iso Date The time this domain was registered in ISO8601 format (Ex: 2019-09-09T16:40:34-04:00)
DBotScore.Indicator String The indicator that was tested.
DBotScore.Score Number The actual score.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.
URL.Data String The URL
URL.Malicious.Vendor String The vendor reporting the URL as malicious.

Command Example

!ipqs-url-reputation url="https://www.example.com"

Context Example

{
    "DBotScore": {
        "Indicator": "https://google.com",
        "Reliability": "A - Completely reliable",
        "Score": 0,
        "Type": "url",
        "Vendor": "IPQualityScore"
    },
    "IPQualityScore": {
        "Url": {
            "adult": false,
            "content_type": "text/html; charset=UTF-8",
            "dns_valid": true,
            "domain": "google.com",
            "domain_age": {
                "human": "24 years ago",
                "iso": "1997-09-15T00:00:00-04:00",
                "timestamp": 874296000
            },
            "domain_rank": 1,
            "ip_address": "172.217.11.142",
            "malware": false,
            "message": "Success.",
            "page_size": 39242,
            "parking": false,
            "phishing": false,
            "request_id": "4DpK9WpOZGQFSPi",
            "risk_score": 0,
            "server": " gws\r\n",
            "spamming": false,
            "status_code": 200,
            "success": true,
            "suspicious": false,
            "unsafe": false,
            "url": "https://google.com"
        }
    },
    "URL": {
        "Data": "https://google.com"
    }
}

Human Readable Output

IPQualityScore Results for https://google.com

message success unsafe domain ip_address server content_type status_code page_size domain_rank dns_valid parking spamming malware phishing suspicious adult risk_score domain_age request_id url
Success. true false google.com 172.217.11.142 gws
text/html; charset=UTF-8 200 39242 1 true false false false false false false 0 human: 24 years ago
timestamp: 874296000
iso: 1997-09-15T00:00:00-04:00
4DpK9WpOZGQFSPi https://google.com

url


Deprecated. Use ipqs-url-reputation instead.

Base Command

url

Input

Argument Name Description Required
url URL(s) to check. Supports comma-separated list. Required

Command Example

!url url="https://www.example.com"


ipqs-phone-reputation


Runs validation and reputation on phone numbers.

Base Command

ipqs-phone-reputation

Input

Argument Name Description Required
phone Phone number(s) to check in international format (e.g. +14155552671). Supports comma-separated list. Required

Context Output

Path Type Description
IPQualityScore.Phone.success Boolean Was the request successful?
IPQualityScore.Phone.message String A generic status message.
IPQualityScore.Phone.formatted String Internationally formatted phone number.
IPQualityScore.Phone.local_format String Local format of the phone number.
IPQualityScore.Phone.valid Boolean Is the phone number valid?
IPQualityScore.Phone.fraud_score Number Overall fraud score (0–100).
IPQualityScore.Phone.recent_abuse Boolean Has there been recent abuse reported for this number?
IPQualityScore.Phone.VOIP Boolean Is the number a VOIP number?
IPQualityScore.Phone.prepaid Boolean Is this a prepaid phone number?
IPQualityScore.Phone.risky Boolean Is this number considered risky?
IPQualityScore.Phone.active Boolean Is this number currently active?
IPQualityScore.Phone.carrier String Carrier name.
IPQualityScore.Phone.line_type String Type of phone line (e.g., Landline, Mobile).
IPQualityScore.Phone.country String Country code or name associated with the number.
IPQualityScore.Phone.city String City associated with the number.
IPQualityScore.Phone.zip_code String Zip code associated with the number.
IPQualityScore.Phone.region String Region associated with the number.
IPQualityScore.Phone.dialing_code Number Country dialing code.
IPQualityScore.Phone.active_status String Active status description.
IPQualityScore.Phone.sms_domain String SMS domain for the number.
IPQualityScore.Phone.associated_email_addresses.status String Status of associated email addresses.
IPQualityScore.Phone.associated_email_addresses.emails Unknown List of email addresses associated with the number.
IPQualityScore.Phone.user_activity String User activity status.
IPQualityScore.Phone.mnc String Mobile Network Code.
IPQualityScore.Phone.mcc String Mobile Country Code.
IPQualityScore.Phone.leaked Boolean Was this number found in a known data leak?
IPQualityScore.Phone.spammer Boolean Is this number a known spammer?
IPQualityScore.Phone.do_not_call Boolean Is this number on a do-not-call list?
IPQualityScore.Phone.name String Name associated with the number.
IPQualityScore.Phone.timezone String Timezone for the number.
IPQualityScore.Phone.tcpa_blacklist Boolean Is the number on a TCPA blacklist?
IPQualityScore.Phone.accurate_country_code Boolean Is the country code accurate?
IPQualityScore.Phone.sms_email String SMS email address for the number.
IPQualityScore.Phone.number_recycling.message String Number recycling status message.
IPQualityScore.Phone.number_recycling.recently_recycled Boolean Was the number recently recycled?
IPQualityScore.Phone.number_recycling.last_ported_date String Last ported date for the number.
IPQualityScore.Phone.number_recycling.ported Boolean Was the number ported?
IPQualityScore.Phone.identity_data String Identity data associated with the number.
IPQualityScore.Phone.request_id String Unique request identifier.
IPQualityScore.Phone.phone String The phone number that was queried.
DBotScore.Indicator String The indicator that was tested.
DBotScore.Score Number The actual DBot score.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.

Command Example

!ipqs-phone-reputation phone="+15555555555"

Context Example

{
    "DBotScore": {
        "Indicator": "+916300445124",
        "Reliability": "A - Completely reliable",
        "Score": 0,
        "Type": "phone",
        "Vendor": "IPQualityScore"
    },
    "IPQualityScore": {
        "Phone": {
            "message": "Phone is valid.",
            "success": true,
            "formatted": "+916300445124",
            "local_format": "063004 45124",
            "valid": true,
            "fraud_score": 0,
            "recent_abuse": false,
            "VOIP": false,
            "prepaid": false,
            "risky": false,
            "active": true,
            "carrier": "Reliance (JIO)",
            "line_type": "Wireless",
            "country": "IN",
            "city": "N/A",
            "zip_code": "N/A",
            "region": "India",
            "dialing_code": 91,
            "active_status": "N/A",
            "sms_domain": "N/A",
            "associated_email_addresses": {
                "status": "No associated emails found.",
                "emails": []
            },
            "user_activity": "high",
            "mnc": "854",
            "mcc": "405",
            "leaked": true,
            "spammer": false,
            "request_id": "gOODapX9Cj",
            "name": "CHETHAN SWAROOP",
            "timezone": "Asia/Kolkata",
            "do_not_call": false,
            "tcpa_blacklist": false,
            "accurate_country_code": false,
            "sms_email": "N/A",
            "number_recycling": {
                "message": "Number recycling feature disabled. Please contact support to activate.",
                "recently_recycled": null,
                "last_ported_date": null,
                "ported": null
            },
            "identity_data": []
        }
    },
    "Phone": {
        "Number": "+916300445124"
    }
}

Human Readable Output

IPQualityScore Results for +15555555555

fraud_score valid active VOIP prepaid risky carrier line_type country phone
20 true true false false false AT&T Mobile US +15555555555

ipqs-username-leaked


Checks if a username has been exposed in known data breaches.

Base Command

ipqs-username-leaked

Input

Argument Name Description Required
username Username(s) to check for leaks. Supports comma-separated list. Required

Context Output

Path Type Description
IPQualityScore.Username.success Boolean Was the request successful?
IPQualityScore.Username.message String Status message.
IPQualityScore.Username.request_hash String Unique hash for the request.
IPQualityScore.Username.source Unknown List of sources where the username was found.
IPQualityScore.Username.exposed Boolean Was the username found in a known leak?
IPQualityScore.Username.first_seen.human String Human-readable time when the leak was first seen.
IPQualityScore.Username.first_seen.timestamp Number Unix epoch when the leak was first seen.
IPQualityScore.Username.first_seen.iso String ISO8601 timestamp when the leak was first seen.
IPQualityScore.Username.request_id String Unique request identifier.
IPQualityScore.Username.username String The username that was queried.
DBotScore.Indicator String The indicator that was tested.
DBotScore.Score Number The actual DBot score.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.

Command Example

!ipqs-username-leaked username="user@123"

Context Example

{
    "DBotScore": {
        "Indicator": "testuser123",
        "Reliability": "A - Completely reliable",
        "Score": 0,
        "Type": "username",
        "Vendor": "IPQualityScore"
    },
    "IPQualityScore": {
        "Username": {
            "success": true,
            "message": "Success",
            "request_hash": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",
            "source": [],
            "exposed": false,
            "first_seen": {
                "human": "just now",
                "timestamp": 1776841128,
                "iso": "2026-04-22T02:58:48-04:00"
            },
            "request_id": "gOOSmfym49"
        }
    },
    "Account": {
        "Username": "testuser123"
    }
}

Human Readable Output

IPQualityScore Results for testuser123

success exposed source request_id username
true false   gOOSmfym49 testuser123

ipqs-password-leaked


Checks if a password has been exposed in known data breaches.

Base Command

ipqs-password-leaked

Input

Argument Name Description Required
password Password(s) to check for leaks. Supports comma-separated list. Required

Context Output

Path Type Description
IPQualityScore.Password.success Boolean Was the request successful?
IPQualityScore.Password.message String Status message.
IPQualityScore.Password.request_hash String Unique hash for the request.
IPQualityScore.Password.source Unknown List of sources where the password was found.
IPQualityScore.Password.exposed Boolean Was the password found in a known leak?
IPQualityScore.Password.first_seen.human String Human-readable time when the leak was first seen.
IPQualityScore.Password.first_seen.timestamp Number Unix epoch when the leak was first seen.
IPQualityScore.Password.first_seen.iso String ISO8601 timestamp when the leak was first seen.
IPQualityScore.Password.request_id String Unique request identifier.
IPQualityScore.Password.password String The password that was queried.
DBotScore.Indicator String The indicator that was tested.
DBotScore.Score Number The actual DBot score.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.

Command Example

!ipqs-password-leaked password="user123"

Context Example

{
    "DBotScore": {
        "Indicator": "abc@123",
        "Reliability": "A - Completely reliable",
        "Score": 3,
        "Type": "password",
        "Vendor": "IPQualityScore"
    },
    "IPQualityScore": {
        "Password": {
            "success": true,
            "message": "Success",
            "request_hash": "4f53cda18c2baa0c0354bb5f9a3ecbe5ed12ab4d8e11ba873c2f11161202b945",
            "source": ["Exploit Antipublic", "Dark Web Leaks"],
            "exposed": true,
            "first_seen": {
                "human": "3 years ago",
                "timestamp": 1681115849,
                "iso": "2023-04-10T04:37:29-04:00"
            },
            "request_id": "gOOeuEg77u"
        }
    }
}

Human Readable Output

IPQualityScore Results for provided password

success exposed source request_id
true true Exploit Antipublic, Dark Web Leaks gOOeuEg77u

ipqs-email-leaked


Checks if an email address has been exposed in known data breaches.

Base Command

ipqs-email-leaked

Input

Argument Name Description Required
email Email address(es) to check for leaks. Supports comma-separated list. Required

Context Output

Path Type Description
IPQualityScore.LeakedEmail.success Boolean Was the request successful?
IPQualityScore.LeakedEmail.message String Status message.
IPQualityScore.LeakedEmail.request_hash String Unique hash for the request.
IPQualityScore.LeakedEmail.source Unknown List of sources where the email was found.
IPQualityScore.LeakedEmail.exposed Boolean Was the email found in a known leak?
IPQualityScore.LeakedEmail.first_seen.human String Human-readable time when the leak was first seen.
IPQualityScore.LeakedEmail.first_seen.timestamp Number Unix epoch when the leak was first seen.
IPQualityScore.LeakedEmail.first_seen.iso String ISO8601 timestamp when the leak was first seen.
IPQualityScore.LeakedEmail.plain_text_password String Plain text password if found in the leak.
IPQualityScore.LeakedEmail.request_id String Unique request identifier.
IPQualityScore.LeakedEmail.email String The email address that was queried.
IPQualityScore.Email.exposed Boolean Was the email found in a known leak?
IPQualityScore.Email.success Boolean Was the request successful?
IPQualityScore.Email.plain_text_password String Plain text password if found in the leak.
IPQualityScore.Email.first_seen.human String Human-readable time when the leak was first seen.
IPQualityScore.Email.first_seen.iso String ISO8601 timestamp when the leak was first seen.
IPQualityScore.Email.first_seen.timestamp Number Unix epoch when the leak was first seen.
IPQualityScore.Email.request_hash String Unique hash for the request.
IPQualityScore.Email.source Unknown List of sources where the email was found.
IPQualityScore.Email.request_id String Unique request identifier.
IPQualityScore.Email.message String Status message.
DBotScore.Indicator String The indicator that was tested.
DBotScore.Score Number The actual DBot score.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.

Command Example

!ipqs-email-leaked email="user@example.com"

Context Example

{
    "DBotScore": {
        "Indicator": "user@example.com",
        "Reliability": "A - Completely reliable",
        "Score": 3,
        "Type": "email",
        "Vendor": "IPQualityScore"
    },
    "IPQualityScore": {
        "LeakedEmail": {
            "success": true,
            "message": "Success",
            "request_hash": "d2b6ff2e611edf5bb40014608a84d75a5b30773272248c28fa79db3bd5739677",
            "source": ["Dark Web Leaks"],
            "exposed": true,
            "first_seen": {
                "human": "6 years ago",
                "timestamp": 1591641120,
                "iso": "2020-06-08T14:32:00-04:00"
            },
            "plain_text_password": false,
            "request_id": "gOOsrUX4lK",
            "email": "user@example.com"
        }
    },
    "Email": {
        "Address": "user@example.com"
    }
}

Human Readable Output

IPQualityScore Results for user@example.com

success exposed plain_text_password source request_id email
true true false Dark Web Leaks gOOsrUX4lK user@example.com

ipqs-file-scan


Uploads a file and scans it using the IPQualityScore Malware File Scanner API. First attempts a cached lookup by file hash; if not cached, the file is submitted for scanning. Polls for results automatically if the scan is pending.

Base Command

ipqs-file-scan

Input

Argument Name Description Required
entry_id The entry id of the uploaded file in the War Room. Required

Context Output

Path Type Description
IPQualityScore.FileScan.success Boolean Was the request successful?
IPQualityScore.FileScan.message String Status message.
IPQualityScore.FileScan.file_name String The file name that was scanned.
IPQualityScore.FileScan.file_hash String SHA256 hash of the file.
IPQualityScore.FileScan.type String Type of operation performed: “scan” or “lookup”.
IPQualityScore.FileScan.status String Scan status: “pending” or “complete”.
IPQualityScore.FileScan.detected Boolean Was the file detected as malicious?
IPQualityScore.FileScan.detected_scans Number Number of engines that detected the file as malicious.
IPQualityScore.FileScan.total_scans Number Total number of scan engines used.
IPQualityScore.FileScan.result String Detailed scan engine results.
IPQualityScore.FileScan.file_size Number File size in bytes.
IPQualityScore.FileScan.file_type String MIME type of the file.
IPQualityScore.FileScan.sha1 String SHA1 hash of the file.
IPQualityScore.FileScan.md5 String MD5 hash of the file.
IPQualityScore.FileScan.update_url String URL to poll for updated scan results.
IPQualityScore.FileScan.request_id String Unique request identifier.
File.SHA256 String SHA256 hash of the file.
File.SHA1 String SHA1 hash of the file.
File.MD5 String MD5 hash of the file.
File.Size Number File size in bytes.
File.Type String File MIME type.
File.Malicious.Count Number Number of detections.
DBotScore.Indicator String The indicator that was tested.
DBotScore.Score Number The actual DBot score.
DBotScore.Type String The indicator type (file).
DBotScore.Vendor String The vendor used to calculate the score.

Command Example

!ipqs-file-scan entry_id="212@_13"

Human Readable Output

IPQualityScore File Scan Result

file_name file_hash detected detected_scans total_scans status file_type file_size sha1 md5 request_id
ipks.exe a35e17ca3d1c0d7363ad4a9c358260d1343033212299ac37cd43471aaa3478b9 false 0 7 done text/html 61049 6751c12507d83f850edaceea8bd51031d27ee363 942af291e78ae29787c0069611ec58e3 fmXlsRjJ4x

ipqs-url-file-scan


Submits a URL to the IPQualityScore Malware File Scanner API. First attempts a cached lookup; if not cached, the URL is submitted for scanning. Polls for results automatically if the scan is pending.

Base Command

ipqs-url-file-scan

Input

Argument Name Description Required
url URL(s) to scan for malware. Supports comma-separated list. Required

Context Output

Path Type Description
IPQualityScore.URLFileScan.success Boolean Was the request successful?
IPQualityScore.URLFileScan.message String Status message.
IPQualityScore.URLFileScan.file_name String The URL that was scanned.
IPQualityScore.URLFileScan.file_hash String SHA256 hash of the scanned content.
IPQualityScore.URLFileScan.type String Type of operation performed: “scan” or “lookup”.
IPQualityScore.URLFileScan.status String Scan status: “pending” or “complete”.
IPQualityScore.URLFileScan.detected Boolean Was the URL detected as malicious?
IPQualityScore.URLFileScan.detected_scans Number Number of engines that detected the URL as malicious.
IPQualityScore.URLFileScan.total_scans Number Total number of scan engines used.
IPQualityScore.URLFileScan.result String Detailed scan engine results.
IPQualityScore.URLFileScan.file_size Number Size of the scanned content in bytes.
IPQualityScore.URLFileScan.file_type String MIME type of the scanned content.
IPQualityScore.URLFileScan.sha1 String SHA1 hash of the scanned content.
IPQualityScore.URLFileScan.md5 String MD5 hash of the scanned content.
IPQualityScore.URLFileScan.update_url String URL to poll for updated scan results.
IPQualityScore.URLFileScan.scan_date Unknown Date and time when the scan was performed.
IPQualityScore.URLFileScan.request_id String Unique request identifier.
URL.Data String The URL that was scanned.
URL.Malicious.Description String Description of why the URL was flagged as malicious.
URL.Malicious.Vendor String The vendor that identified the URL as malicious.
DBotScore.Indicator String The indicator that was tested.
DBotScore.Score Number The actual DBot score.
DBotScore.Type String The indicator type (URL).
DBotScore.Vendor String The vendor used to calculate the score.

Command Example

!ipqs-url-file-scan url="http://www.example.com/"

Human Readable Output

IPQualityScore URL Scan Result

file_name file_hash detected detected_scans total_scans status file_type file_size sha1 md5 request_id
M94scZj.exe a35e17ca3d1c0d7363ad4a9c358260d1343033212299ac37cd43471aaa3478b9 false 0 7 done text/html 61049 6751c12507d83f850edaceea8bd51031d27ee363 942af291e78ae29787c0069611ec58e3 fmXlsRjJ4x

Scoring Thresholds

Indicator Type Suspicious (default) Malicious (default) Score Field
IP 75 90 fraud_score
Email 75 90 fraud_score
URL (reputation) 75 90 risk_score
Phone 75 90 fraud_score
File (malware scan) 1 detection 5 detections detected_scans
URL (malware scan) 1 detection 5 detections detected_scans

Configuration parameters

  • apikey — API Key (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • ip_suspicious_score_threshold — IP Suspicious Score Threshold
  • ip_malicious_score_threshold — IP Malicious Score Threshold
  • email_suspicious_score_threshold — Email Suspicious Score Threshold
  • email_malicious_score_threshold — Email Malicious Score Threshold
  • url_suspicious_score_threshold — Url Suspicious Score Threshold
  • url_malicious_score_threshold — Url Malicious Score Threshold
  • phone_suspicious_score_threshold — Phone Suspicious Score Threshold
  • phone_malicious_score_threshold — Phone Malicious Score Threshold
  • feedReliability — Source Reliability (required)

Commands (12)

  • email Deprecated

    Deprecated. Use ipqs-email-reputation instead.

  • ip Deprecated

    Deprecated. Use ipqs-ip-reputation instead.

  • ipqs-email-leaked

    Checks if an email has been leaked in known breaches.

  • ipqs-email-reputation

    Runs reputation on email addresses.

  • ipqs-file-scan

    Uploads and scans a file using IPQualityScore Malware File Scanner API.

  • ipqs-ip-reputation

    Runs reputation on IPs.

  • ipqs-password-leaked

    Checks if a password has been leaked in known breaches.

  • ipqs-phone-reputation

    Runs validation and reputation on phone numbers.

  • ipqs-url-file-scan

    Scans a URL using IPQualityScore Malware File Scanner API.

  • ipqs-url-reputation

    Runs reputation on URLs.

  • ipqs-username-leaked

    Checks if a username has been leaked in known breaches.

  • url Deprecated

    Deprecated. Use ipqs-url-reputation instead.

"""
IPQualityScore integration for XSOAR.

Supports:
- IP reputation
- URL/domain reputation
- Email reputation
- Phone reputation
- Leak checks (username, password, email)
- Malware scan / lookup for files and URLs
"""

from collections.abc import Callable
from ipaddress import ip_address
from re import compile as re_compile
from typing import Any
from urllib.parse import quote, urlparse

import demistomock as demisto  # pylint: disable=import-error
import urllib3
from CommonServerPython import *  # pylint: disable=import-error,unused-wildcard-import,wildcard-import
from CommonServerUserPython import *  # pylint: disable=import-error

INTEGRATION_NAME = "IPQualityScore"
BASE_URL = "https://ipqualityscore.com/api/json"
MALWARE_SCAN_URL = f"{BASE_URL}/malware/scan/"
MALWARE_LOOKUP_URL = f"{BASE_URL}/malware/lookup/"

DEFAULT_THRESHOLD_VALUES = {"suspicious": 75, "malicious": 90}
DEFAULT_FILE_THRESHOLD_VALUES = {"suspicious": 1, "malicious": 4}

DEFAULT_MAX_RETRIES = 9
DEFAULT_POLLING_INTERVAL = 10

REPUTATION_RELIABILITY_MAP = {
    "A+ - 3rd party enrichment": DBotScoreReliability.A_PLUS,
    "A - Completely reliable": DBotScoreReliability.A,
    "B - Usually reliable": DBotScoreReliability.B,
    "C - Fairly reliable": DBotScoreReliability.C,
    "D - Not usually reliable": DBotScoreReliability.D,
    "E - Unreliable": DBotScoreReliability.E,
    "F - Reliability cannot be judged": DBotScoreReliability.F,
}

EMAIL_REGEX = re_compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
DOMAIN_REGEX = re_compile(
    r"^(?=.{1,253}$)(?!-)(?:[A-Za-z0-9-]{1,63}\.)+[A-Za-z]{2,63}$",
)
PHONE_REGEX = re_compile(r"^\+?[0-9().\-\s]{6,25}$")


class Client(BaseClient):
    """Client for IPQualityScore API."""

    def reputation_request(
        self,
        endpoint: str,
        query_name: str,
        value: str,
    ) -> dict[str, Any]:
        """Send reputation request to IPQS."""
        encoded_value = quote(value, safe="")
        return ensure_dict_response(
            self._http_request(
                method="GET",
                url_suffix=f"/{endpoint}/?{query_name}={encoded_value}",
                resp_type="json",
            ),
            f"{endpoint} reputation request",
        )

    def leaked_request(
        self,
        leaked_type: str,
        query_name: str,
        value: str,
    ) -> dict[str, Any]:
        """Send leaked-data request to IPQS."""
        encoded_value = quote(value, safe="")
        return ensure_dict_response(
            self._http_request(
                method="GET",
                url_suffix=f"/leaked/{leaked_type}?{query_name}={encoded_value}",
                resp_type="json",
            ),
            f"{leaked_type} leaked request",
        )

    def malware_url_request(self, *, is_lookup: bool, url: str) -> dict[str, Any]:
        """Submit URL malware lookup or scan request."""
        return ensure_dict_response(
            self._http_request(
                method="POST",
                full_url=MALWARE_LOOKUP_URL if is_lookup else MALWARE_SCAN_URL,
                data={"url": url},
                resp_type="json",
                timeout=60,
            ),
            "malware url lookup" if is_lookup else "malware url scan",
        )

    def malware_file_request(
        self,
        *,
        is_lookup: bool,
        file_path: str,
    ) -> dict[str, Any]:
        """Submit file malware lookup or scan request."""
        with open(file_path, "rb") as file_handle:
            return ensure_dict_response(
                self._http_request(
                    method="POST",
                    full_url=MALWARE_LOOKUP_URL if is_lookup else MALWARE_SCAN_URL,
                    files={"file": file_handle},
                    resp_type="json",
                    timeout=60,
                ),
                "malware file lookup" if is_lookup else "malware file scan",
            )

    def poll_result(self, request_id: str) -> dict[str, Any]:
        """Fetch IPQS postback result once."""
        return ensure_dict_response(
            self._http_request(
                method="POST",
                url_suffix="/postback",
                data={"request_id": request_id},
                resp_type="json",
                timeout=30,
            ),
            "polling scan result",
        )


def ensure_dict_response(response: Any, operation: str) -> dict[str, Any]:
    """Validate that the response is a dictionary."""
    if not isinstance(response, dict):
        raise DemistoException(
            f"Invalid response from {operation}: expected dict, got {type(response).__name__}",
        )
    return response


def get_reputation_reliability(reliability: str | None) -> str:
    """Map configured feed reliability to DBotScore reliability."""
    return REPUTATION_RELIABILITY_MAP.get(reliability or "", DBotScoreReliability.F)


def calculate_score(
    value: int,
    suspicious_threshold: int,
    malicious_threshold: int,
) -> int:
    """Convert a numeric value into a DBot score."""
    if value >= malicious_threshold:
        return Common.DBotScore.BAD
    if value >= suspicious_threshold:
        return Common.DBotScore.SUSPICIOUS
    return Common.DBotScore.NONE


def was_leaked(result: dict[str, Any]) -> bool:
    """Support both possible leaked indicators from API schema."""
    return bool(result.get("leaked") or result.get("exposed"))


def get_thresholds(params: dict[str, Any], prefix: str) -> tuple[int, int]:
    """Get suspicious and malicious score thresholds from integration params."""
    suspicious = arg_to_number(params.get(f"{prefix}_suspicious_score_threshold"))
    malicious = arg_to_number(params.get(f"{prefix}_malicious_score_threshold"))

    return (
        suspicious if suspicious is not None else DEFAULT_THRESHOLD_VALUES["suspicious"],
        malicious if malicious is not None else DEFAULT_THRESHOLD_VALUES["malicious"],
    )


def get_file_thresholds(params: dict[str, Any]) -> tuple[int, int]:
    """Get suspicious and malicious thresholds for malware scan results."""
    suspicious = arg_to_number(params.get("file_suspicious_score_threshold"))
    malicious = arg_to_number(params.get("file_malicious_score_threshold"))

    return (
        suspicious if suspicious is not None else DEFAULT_FILE_THRESHOLD_VALUES["suspicious"],
        malicious if malicious is not None else DEFAULT_FILE_THRESHOLD_VALUES["malicious"],
    )


def get_retry_context_key(request_id: str) -> str:
    """Build retry counter key for integration context."""
    return f"ipqs_retry_count_{request_id}"


def get_retry_count(request_id: str) -> int:
    """Get retry count from integration context."""
    context = demisto.getIntegrationContext() or {}
    retry_count = context.get(get_retry_context_key(request_id), 0)

    if isinstance(retry_count, int):
        return retry_count

    if isinstance(retry_count, str):
        try:
            return int(retry_count)
        except ValueError as exc:
            raise DemistoException(
                f"Invalid retry count for request_id: {request_id}",
            ) from exc

    raise DemistoException(
        f"Invalid retry count type for request_id {request_id}: {type(retry_count).__name__}",
    )


def set_retry_count(request_id: str, retry_count: int) -> None:
    """Set retry count in integration context."""
    context = demisto.getIntegrationContext() or {}
    context[get_retry_context_key(request_id)] = retry_count
    demisto.setIntegrationContext(context)


def clear_retry_count(request_id: str) -> None:
    """Clear retry count after scan completes or fails."""
    context = demisto.getIntegrationContext() or {}
    context.pop(get_retry_context_key(request_id), None)
    demisto.setIntegrationContext(context)


def get_dbot_type(type_name: str, fallback: str = DBotScoreType.URL) -> str:
    """Safely get DBotScoreType value if available."""
    return getattr(DBotScoreType, type_name, fallback)


def create_dbot_score(
    indicator: str,
    indicator_type: str,
    score: int,
    reliability: str,
) -> Common.DBotScore:
    """Create a DBotScore object."""
    return Common.DBotScore(
        indicator=indicator,
        indicator_type=indicator_type,
        score=score,
        integration_name=INTEGRATION_NAME,
        reliability=reliability,
    )


def create_command_result(
    title: str,
    result: dict[str, Any],
    indicator: Any,
    outputs_prefix: str,
    outputs_key_field: str,
) -> CommandResults:
    """Create a standard CommandResults object."""
    return CommandResults(
        readable_output=tableToMarkdown(title, result, headers=list(result.keys())),
        indicator=indicator,
        outputs_prefix=outputs_prefix,
        outputs_key_field=outputs_key_field,
        outputs=result,
        raw_response=result,
    )


def build_pending_result(
    args: dict[str, Any],
    result: dict[str, Any],
    request_id: str,
    outputs_prefix: str,
) -> CommandResults:
    """Return scheduled command for pending IPQS malware scan."""
    current_retry = get_retry_count(request_id)

    if current_retry >= DEFAULT_MAX_RETRIES:
        clear_retry_count(request_id)
        raise DemistoException(
            f"Maximum retries reached while waiting for IPQS malware scan result. " f"Request ID: {request_id}",
        )

    set_retry_count(request_id, current_retry + 1)

    scheduled_args = {
        **args,
        "request_id": request_id,
    }

    return CommandResults(
        readable_output=(
            f"IPQS malware scan is still processing. "
            f"The result will be checked again in {DEFAULT_POLLING_INTERVAL} seconds. "
            f"Polling is limited to {DEFAULT_MAX_RETRIES} attempts. "
            f"Request ID: {request_id}"
        ),
        outputs_prefix=outputs_prefix,
        outputs=result,
        raw_response=result,
        scheduled_command=ScheduledCommand(
            command=demisto.command(),
            next_run_in_seconds=DEFAULT_POLLING_INTERVAL,
            args=scheduled_args,
        ),
    )


def flatten_engine_results(scan_result: dict[str, Any]) -> dict[str, Any]:
    """Flatten engine results into top-level keys for readable output."""
    flattened = dict(scan_result)
    engines = flattened.pop("result", [])

    if not isinstance(engines, list):
        return flattened

    for engine in engines:
        if not isinstance(engine, dict):
            continue

        engine_name = engine.get("name", "Unknown")
        flattened[engine_name] = {
            "detected": engine.get("detected", False),
            "error": engine.get("error", False),
        }

    return flattened


def extract_detected_scans(scan_result: dict[str, Any]) -> int:
    """Extract detected scan count from engine list, or fallback to API field."""
    engines = scan_result.get("result")

    if isinstance(engines, list) and engines:
        return sum(1 for engine in engines if isinstance(engine, dict) and engine.get("detected") is True)

    detected_scans = arg_to_number(scan_result.get("detected_scans"))
    return detected_scans if detected_scans is not None else 0


def normalize_scan_result(scan_result: dict[str, Any]) -> dict[str, Any]:
    """Normalize scan result fields used by XSOAR."""
    normalized = dict(scan_result)
    normalized.pop("update_url", None)
    normalized["file_size"] = arg_to_number(normalized.get("file_size")) or 0
    normalized["detected_scans"] = extract_detected_scans(normalized)
    return normalized


def validate_ip(ip_value: str) -> str:
    """Validate IP value."""
    try:
        stripped_ip = ip_value.strip()
        ip_address(stripped_ip)
        return stripped_ip
    except ValueError as exc:
        raise DemistoException(f"Invalid IP address provided: {ip_value}") from exc


def validate_email(email: str) -> str:
    """Validate email value."""
    email = email.strip()
    if not EMAIL_REGEX.match(email):
        raise DemistoException(f"Invalid email address provided: {email}")
    return email


def validate_phone(phone: str) -> str:
    """Validate phone value."""
    phone = phone.strip()
    if not PHONE_REGEX.match(phone):
        raise DemistoException(f"Invalid phone number provided: {phone}")
    return phone


def validate_url_or_domain(value: str) -> str:
    """Validate URL or domain. Both use the same IPQS endpoint."""
    value = value.strip()
    parsed = urlparse(value)

    if parsed.scheme and parsed.netloc:
        return value

    if DOMAIN_REGEX.match(value):
        return value

    raise DemistoException(f"Invalid URL or domain provided: {value}")


def validate_non_empty(value: str, field_name: str) -> str:
    """Validate non-empty string."""
    value = value.strip()
    if not value:
        raise DemistoException(f"{field_name} cannot be empty")
    return value


def build_reputation_results(
    indicators: list[str],
    fetch_result: Callable[[str], dict[str, Any]],
    validator: Callable[[str], str],
    score_field: str,
    suspicious_threshold: int,
    malicious_threshold: int,
    reliability: str | None,
    indicator_type: str,
    outputs_prefix: str,
    outputs_key_field: str,
    title_template: str,
    indicator_builder: Callable[[str, dict[str, Any], Common.DBotScore], Any],
    result_key_name: str,
) -> list[CommandResults]:
    """Generic builder for reputation-based commands."""
    results: list[CommandResults] = []
    parsed_reliability = get_reputation_reliability(reliability)

    for raw_indicator in indicators:
        indicator_value = validator(raw_indicator)
        result = fetch_result(indicator_value)
        result[result_key_name] = indicator_value

        score_value = arg_to_number(result.get(score_field)) or 0
        score = calculate_score(
            score_value,
            suspicious_threshold,
            malicious_threshold,
        )

        dbot_score = create_dbot_score(
            indicator_value,
            indicator_type,
            score,
            parsed_reliability,
        )

        results.append(
            create_command_result(
                title_template.format(indicator=indicator_value),
                result,
                indicator_builder(indicator_value, result, dbot_score),
                outputs_prefix,
                outputs_key_field,
            ),
        )

    return results


def build_leaked_results(
    indicators: list[str],
    fetch_result: Callable[[str], dict[str, Any]],
    validator: Callable[[str], str],
    reliability: str | None,
    indicator_type: str,
    outputs_prefix: str,
    outputs_key_field: str,
    title_template: str,
    result_key_name: str,
    indicator_builder: Callable[[str, dict[str, Any], Common.DBotScore], Any],
) -> list[CommandResults]:
    """Generic builder for leaked-data commands."""
    results: list[CommandResults] = []
    parsed_reliability = get_reputation_reliability(reliability)

    for raw_indicator in indicators:
        indicator_value = validator(raw_indicator)
        result = fetch_result(indicator_value)
        result[result_key_name] = indicator_value

        dbot_score = create_dbot_score(
            indicator_value,
            indicator_type,
            Common.DBotScore.BAD if was_leaked(result) else Common.DBotScore.NONE,
            parsed_reliability,
        )

        results.append(
            create_command_result(
                title_template.format(indicator=indicator_value),
                result,
                indicator_builder(indicator_value, result, dbot_score),
                outputs_prefix,
                outputs_key_field,
            ),
        )

    return results


def ip_command(
    client: Client,
    args: dict[str, Any],
    suspicious_threshold: int,
    malicious_threshold: int,
    reliability: str | None,
) -> list[CommandResults]:
    """Run reputation check on IP addresses."""
    return build_reputation_results(
        indicators=argToList(args.get("ip"), ","),
        fetch_result=lambda value: client.reputation_request("ip", "ip", value),
        validator=validate_ip,
        score_field="fraud_score",
        suspicious_threshold=suspicious_threshold,
        malicious_threshold=malicious_threshold,
        reliability=reliability,
        indicator_type=DBotScoreType.IP,
        outputs_prefix="IPQualityScore.IP",
        outputs_key_field="address",
        title_template="IPQS Fraud and Risk Scoring Results for IP Address {indicator}",
        result_key_name="address",
        indicator_builder=lambda ip_value, result, dbot_score: Common.IP(
            ip=ip_value,
            dbot_score=dbot_score,
            asn=result.get("ASN"),
            hostname=result.get("host"),
            geo_country=result.get("country_code"),
            geo_longitude=result.get("longitude"),
            geo_latitude=result.get("latitude"),
        ),
    )


def email_command(
    client: Client,
    args: dict[str, Any],
    suspicious_threshold: int,
    malicious_threshold: int,
    reliability: str | None,
) -> list[CommandResults]:
    """Run reputation check on email addresses."""
    return build_reputation_results(
        indicators=argToList(args.get("email"), ","),
        fetch_result=lambda value: client.reputation_request("email", "email", value),
        validator=validate_email,
        score_field="fraud_score",
        suspicious_threshold=suspicious_threshold,
        malicious_threshold=malicious_threshold,
        reliability=reliability,
        indicator_type=DBotScoreType.EMAIL,
        outputs_prefix="IPQualityScore.Email",
        outputs_key_field="address",
        title_template="IPQS Fraud and Risk Scoring Results for Email Address {indicator}",
        result_key_name="address",
        indicator_builder=lambda email_value, result, dbot_score: Common.EMAIL(
            address=email_value,
            dbot_score=dbot_score,
            domain=result.get("sanitized_email", email_value).split("@")[-1],
        ),
    )


def url_command(
    client: Client,
    args: dict[str, Any],
    suspicious_threshold: int,
    malicious_threshold: int,
    reliability: str | None,
) -> list[CommandResults]:
    """Run reputation check on URLs/domains."""
    return build_reputation_results(
        indicators=argToList(args.get("url"), ","),
        fetch_result=lambda value: client.reputation_request("url", "url", value),
        validator=validate_url_or_domain,
        score_field="risk_score",
        suspicious_threshold=suspicious_threshold,
        malicious_threshold=malicious_threshold,
        reliability=reliability,
        indicator_type=DBotScoreType.URL,
        outputs_prefix="IPQualityScore.Url",
        outputs_key_field="url",
        title_template="IPQS Fraud and Risk Scoring Results for URL {indicator}",
        result_key_name="url",
        indicator_builder=lambda url_value, _result, dbot_score: Common.URL(
            url=url_value,
            dbot_score=dbot_score,
        ),
    )


def phone_command(
    client: Client,
    args: dict[str, Any],
    suspicious_threshold: int,
    malicious_threshold: int,
    reliability: str | None,
) -> list[CommandResults]:
    """Run reputation check on phone numbers."""
    phone_dbot_type = get_dbot_type("PHONE", DBotScoreType.URL)

    return build_reputation_results(
        indicators=argToList(args.get("phone"), ","),
        fetch_result=lambda value: client.reputation_request("phone", "phone", value),
        validator=validate_phone,
        score_field="fraud_score",
        suspicious_threshold=suspicious_threshold,
        malicious_threshold=malicious_threshold,
        reliability=reliability,
        indicator_type=phone_dbot_type,
        outputs_prefix="IPQualityScore.Phone",
        outputs_key_field="phone",
        title_template="IPQS Fraud and Risk Scoring Results for Phone Number {indicator}",
        result_key_name="phone",
        indicator_builder=lambda _phone, _result, dbot_score: dbot_score,
    )


def leaked_username_command(
    client: Client,
    args: dict[str, Any],
    reliability: str | None,
) -> list[CommandResults]:
    """Check if usernames were leaked."""
    account_dbot_type = get_dbot_type("ACCOUNT", DBotScoreType.URL)

    return build_leaked_results(
        indicators=argToList(args.get("username"), ","),
        fetch_result=lambda value: client.leaked_request("username", "username", value),
        validator=lambda value: validate_non_empty(value, "Username"),
        reliability=reliability,
        indicator_type=account_dbot_type,
        outputs_prefix="IPQualityScore.Username",
        outputs_key_field="username",
        title_template="IPQS Dark Web Leak Results for Username {indicator}",
        result_key_name="username",
        indicator_builder=lambda _username, _result, dbot_score: dbot_score,
    )


def leaked_password_command(
    client: Client,
    args: dict[str, Any],
    reliability: str | None,
) -> list[CommandResults]:
    """Check if passwords were leaked."""
    generic_dbot_type = get_dbot_type("GENERIC", DBotScoreType.URL)

    return build_leaked_results(
        indicators=argToList(args.get("password"), ","),
        fetch_result=lambda value: client.leaked_request("password", "password", value),
        validator=lambda value: validate_non_empty(value, "Password"),
        reliability=reliability,
        indicator_type=generic_dbot_type,
        outputs_prefix="IPQualityScore.Password",
        outputs_key_field="password",
        title_template="IPQS Dark Web Leak Results for Password {indicator}",
        result_key_name="password",
        indicator_builder=lambda _password, _result, dbot_score: dbot_score,
    )


def leaked_email_command(
    client: Client,
    args: dict[str, Any],
    reliability: str | None,
) -> list[CommandResults]:
    """Check if emails were leaked."""
    return build_leaked_results(
        indicators=argToList(args.get("email"), ","),
        fetch_result=lambda value: client.leaked_request("email", "email", value),
        validator=validate_email,
        reliability=reliability,
        indicator_type=DBotScoreType.EMAIL,
        outputs_prefix="IPQualityScore.LeakedEmail",
        outputs_key_field="email",
        title_template="IPQS Dark Web Leak Results for Email Address {indicator}",
        result_key_name="email",
        indicator_builder=lambda email_value, _result, dbot_score: Common.EMAIL(
            address=email_value,
            dbot_score=dbot_score,
        ),
    )


def build_file_scan_command_result(
    scan_result: dict[str, Any],
    file_name: str,
    suspicious_threshold: int,
    malicious_threshold: int,
    parsed_reliability: str,
) -> CommandResults:
    """Build file scan CommandResults."""
    detected_scans = arg_to_number(scan_result.get("detected_scans")) or 0
    score = calculate_score(
        detected_scans,
        suspicious_threshold,
        malicious_threshold,
    )

    indicator_value = scan_result.get("file_hash") or scan_result.get("sha256") or scan_result.get("md5") or file_name

    dbot_score = create_dbot_score(
        indicator_value,
        DBotScoreType.FILE,
        score,
        parsed_reliability,
    )

    file_context = Common.File(
        sha256=scan_result.get("file_hash") or scan_result.get("sha256"),
        md5=scan_result.get("md5"),
        sha1=scan_result.get("sha1"),
        size=scan_result.get("file_size"),
        name=file_name,
        dbot_score=dbot_score,
    )

    flattened_result = flatten_engine_results(scan_result)
    flattened_result["file_name"] = file_name

    return create_command_result(
        f"IPQS Malware File Scan Results for File {file_name}",
        flattened_result,
        file_context,
        "IPQualityScore.FileScan",
        "file_name",
    )


def build_url_file_scan_command_result(
    scan_result: dict[str, Any],
    url_value: str,
    suspicious_threshold: int,
    malicious_threshold: int,
    parsed_reliability: str,
) -> CommandResults:
    """Build URL malware scan CommandResults."""
    detected_scans = arg_to_number(scan_result.get("detected_scans")) or 0
    score = calculate_score(
        detected_scans,
        suspicious_threshold,
        malicious_threshold,
    )

    dbot_score = create_dbot_score(
        url_value,
        DBotScoreType.URL,
        score,
        parsed_reliability,
    )

    url_context = Common.URL(url=url_value, dbot_score=dbot_score)
    flattened_result = flatten_engine_results(scan_result)

    return create_command_result(
        f"IPQS Malware File Scan Results for URL {url_value}",
        flattened_result,
        url_context,
        "IPQualityScore.URLFileScan",
        "url",
    )


def file_command(
    client: Client,
    args: dict[str, Any],
    suspicious_threshold: int,
    malicious_threshold: int,
    reliability: str | None,
) -> CommandResults | list[CommandResults]:
    """Run malware scan on uploaded files."""
    request_id = args.get("request_id")
    parsed_reliability = get_reputation_reliability(reliability)

    if request_id:
        scan_result = client.poll_result(request_id)

        if scan_result.get("status") == "pending":
            return build_pending_result(
                args=args,
                result=scan_result,
                request_id=request_id,
                outputs_prefix="IPQualityScore.FileScan",
            )

        clear_retry_count(request_id)
        scan_result = normalize_scan_result(scan_result)
        file_name = scan_result.get("file_name") or args.get("entry_id") or request_id

        return build_file_scan_command_result(
            scan_result=scan_result,
            file_name=file_name,
            suspicious_threshold=suspicious_threshold,
            malicious_threshold=malicious_threshold,
            parsed_reliability=parsed_reliability,
        )

    entry_ids = argToList(args.get("entry_id"))
    if not entry_ids:
        raise DemistoException("entry_id is required for file scan.")
    if len(entry_ids) > 1:
        raise DemistoException(
            "Only one entry_id may be submitted for a new file scan request. " "Polling multiple files is not supported.",
        )

    entry_id = entry_ids[0]
    file_info = demisto.getFilePath(entry_id)
    file_path = file_info.get("path")
    file_name = file_info.get("name") or entry_id

    if not file_path:
        raise DemistoException(
            f"Could not resolve file path for entry ID: {entry_id}",
        )

    lookup_response = client.malware_file_request(
        is_lookup=True,
        file_path=file_path,
    )

    if lookup_response.get("status") == "cached":
        scan_result = normalize_scan_result(lookup_response)
        return build_file_scan_command_result(
            scan_result=scan_result,
            file_name=file_name,
            suspicious_threshold=suspicious_threshold,
            malicious_threshold=malicious_threshold,
            parsed_reliability=parsed_reliability,
        )

    scan_result = client.malware_file_request(
        is_lookup=False,
        file_path=file_path,
    )
    request_id = scan_result.get("request_id")

    if scan_result.get("status") == "pending":
        if not request_id:
            raise DemistoException("File scan is pending but request_id is missing.")

        pending_args = dict(args)
        pending_args["entry_id"] = entry_id

        return build_pending_result(
            args=pending_args,
            result=scan_result,
            request_id=request_id,
            outputs_prefix="IPQualityScore.FileScan",
        )

    scan_result = normalize_scan_result(scan_result)
    return build_file_scan_command_result(
        scan_result=scan_result,
        file_name=file_name,
        suspicious_threshold=suspicious_threshold,
        malicious_threshold=malicious_threshold,
        parsed_reliability=parsed_reliability,
    )


def url_file_command(
    client: Client,
    args: dict[str, Any],
    suspicious_threshold: int,
    malicious_threshold: int,
    reliability: str | None,
) -> CommandResults | list[CommandResults]:
    """Run malware scan on URLs/domains."""
    request_id = args.get("request_id")
    parsed_reliability = get_reputation_reliability(reliability)

    if request_id:
        scan_result = client.poll_result(request_id)

        if scan_result.get("status") == "pending":
            return build_pending_result(
                args=args,
                result=scan_result,
                request_id=request_id,
                outputs_prefix="IPQualityScore.URLFileScan",
            )

        clear_retry_count(request_id)
        url_value = args.get("url") or scan_result.get("url") or request_id
        scan_result["url"] = url_value
        scan_result = normalize_scan_result(scan_result)

        return build_url_file_scan_command_result(
            scan_result=scan_result,
            url_value=url_value,
            suspicious_threshold=suspicious_threshold,
            malicious_threshold=malicious_threshold,
            parsed_reliability=parsed_reliability,
        )

    urls = argToList(args.get("url"), ",")
    if not urls:
        raise DemistoException("url is required for URL malware scan.")
    if len(urls) > 1:
        raise DemistoException(
            "Only one URL may be submitted for a new URL file scan request. " "Polling multiple URLs is not supported.",
        )

    url_value = validate_url_or_domain(urls[0])

    lookup_response = client.malware_url_request(
        is_lookup=True,
        url=url_value,
    )

    if lookup_response.get("status") == "cached":
        lookup_response["url"] = url_value
        scan_result = normalize_scan_result(lookup_response)
        return build_url_file_scan_command_result(
            scan_result=scan_result,
            url_value=url_value,
            suspicious_threshold=suspicious_threshold,
            malicious_threshold=malicious_threshold,
            parsed_reliability=parsed_reliability,
        )

    scan_result = client.malware_url_request(
        is_lookup=False,
        url=url_value,
    )
    request_id = scan_result.get("request_id")

    if scan_result.get("status") == "pending":
        if not request_id:
            raise DemistoException("URL scan is pending but request_id is missing.")

        pending_args = dict(args)
        pending_args["url"] = url_value

        return build_pending_result(
            args=pending_args,
            result=scan_result,
            request_id=request_id,
            outputs_prefix="IPQualityScore.URLFileScan",
        )

    scan_result["url"] = url_value
    scan_result = normalize_scan_result(scan_result)
    return build_url_file_scan_command_result(
        scan_result=scan_result,
        url_value=url_value,
        suspicious_threshold=suspicious_threshold,
        malicious_threshold=malicious_threshold,
        parsed_reliability=parsed_reliability,
    )


def test_module(client: Client) -> str:
    """Test integration connectivity."""
    result = client.reputation_request("ip", "ip", "8.8.8.8")
    if result.get("success") is True:
        return "ok"

    raise DemistoException(result.get("message", "Test failed"))


def main() -> None:
    """Main execution entry point."""
    params = demisto.params()
    args = demisto.args()
    command = demisto.command()

    api_key = params.get("apikey")
    if not api_key:
        return_error("API Key is required.")

    verify_certificate = not params.get("insecure", False)
    proxy = params.get("proxy", False)
    reliability = params.get("feedReliability")

    if not verify_certificate:
        demisto.debug("SSL verification is disabled.")
        urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

    client = Client(
        base_url=BASE_URL,
        headers={"IPQS-KEY": api_key},
        verify=verify_certificate,
        proxy=proxy,
    )

    demisto.debug(f"Command being called is {command}")

    def run_ip_command() -> list[CommandResults]:
        suspicious, malicious = get_thresholds(params, "ip")
        return ip_command(client, args, suspicious, malicious, reliability)

    def run_email_command() -> list[CommandResults]:
        suspicious, malicious = get_thresholds(params, "email")
        return email_command(client, args, suspicious, malicious, reliability)

    def run_phone_command() -> list[CommandResults]:
        suspicious, malicious = get_thresholds(params, "phone")
        return phone_command(client, args, suspicious, malicious, reliability)

    def run_url_command() -> list[CommandResults]:
        suspicious, malicious = get_thresholds(params, "url")
        return url_command(client, args, suspicious, malicious, reliability)

    def run_file_scan_command() -> CommandResults | list[CommandResults]:
        suspicious, malicious = get_file_thresholds(params)
        return file_command(client, args, suspicious, malicious, reliability)

    def run_url_file_scan_command() -> CommandResults | list[CommandResults]:
        suspicious, malicious = get_file_thresholds(params)
        return url_file_command(client, args, suspicious, malicious, reliability)

    commands: dict[str, Callable[[], Any]] = {
        "test-module": lambda: test_module(client),
        "ipqs-ip-reputation": run_ip_command,
        "ip": run_ip_command,  # Deprecated: use ipqs-ip-reputation
        "ipqs-email-reputation": run_email_command,
        "email": run_email_command,  # Deprecated: use ipqs-email-reputation
        "ipqs-phone-reputation": run_phone_command,
        "ipqs-url-reputation": run_url_command,
        "url": run_url_command,  # Deprecated: use ipqs-url-reputation
        "ipqs-username-leaked": lambda: leaked_username_command(
            client,
            args,
            reliability,
        ),
        "ipqs-password-leaked": lambda: leaked_password_command(
            client,
            args,
            reliability,
        ),
        "ipqs-email-leaked": lambda: leaked_email_command(client, args, reliability),
        "ipqs-file-scan": run_file_scan_command,
        "ipqs-url-file-scan": run_url_file_scan_command,
    }

    try:
        command_func = commands.get(command)
        if command_func is None:
            raise NotImplementedError(f"Command '{command}' is not implemented.")

        return_results(command_func())

    except Exception as exc:  # pylint: disable=broad-exception-caught
        return_error(f"Failed to execute {command} command. Error: {exc!s}")


if __name__ in ("__main__", "__builtin__", "builtins"):
    main()