CriminalIP

Criminal IP is a comprehensive cyber threat intelligence solution that provides actionable insights into IP addresses, domains, and connected assets across the internet. It enables organizations to detect malicious indicators, assess asset reputation, and enhance threat detection by integrating enriched threat data directly into security operations via the XSOAR interface.

Data Enrichment & Threat Intelligence · CriminalIP

Details

IDCriminalIP
ProviderAI Spera
CategoryData Enrichment & Threat Intelligence
From Version6.10.0
Docker Imagedemisto/python3:3.12.11.4508456

README

Criminal IP is a comprehensive cyber threat intelligence solution that provides actionable insights into IP addresses, domains, and connected assets across the internet.
It enables organizations to detect malicious indicators, assess asset reputation, and enhance threat detection by integrating enriched threat data directly into security operations via the XSOAR interface.

This integration was integrated and tested with version 1.0.0 of CriminalIP.

Configure CriminalIP in Cortex

Parameter Description Required
API Key The API Key to use for connection False
Server URL The base URL of the Criminal IP API. True
Trust any certificate (not secure) When set to true, SSL certificates will not be validated. False
Use system proxy settings Use the system proxy settings to communicate with the API. False
Request timeout (seconds) Timeout for HTTP requests in seconds. Default is 30. False

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.

criminal-ip-ip-report


Provides detailed information about an IP address using Criminal IP’s API.

Base Command

criminal-ip-ip-report

Input

Argument Name Description Required
ip The IP address to search. Required

Context Output

Path Type Description
CriminalIP.IP.IP String Queried IP address.
CriminalIP.IP.InboundScore String Inbound reputation score.
CriminalIP.IP.OutboundScore String Outbound reputation score.
CriminalIP.IP.Issues String Detected issues (VPN, Proxy, Tor, Hosting, Cloud, etc.)
CriminalIP.IP.ProtectedIPs Number Number of protected IPs related to this IP.
CriminalIP.IP.RelatedDomains Number Number of domains related to this IP.
CriminalIP.IP.ASN Number Autonomous System Number (ASN).
CriminalIP.IP.ASName String Autonomous System Name.
CriminalIP.IP.Org String Organization name from Whois.
CriminalIP.IP.Country String Country code from Whois.
CriminalIP.IP.Hostname String Resolved hostname.
CriminalIP.IP.OpenPorts Number Number of open ports detected.
CriminalIP.IP.ObservedPort Number Example open port number.
CriminalIP.IP.ObservedService String Example service detected on an open port.
CriminalIP.IP.Vulnerabilities Number Number of vulnerabilities detected on the IP.
CriminalIP.IP.ObservedCVE String Example CVE ID detected on the IP.
CriminalIP.IP.ObservedCVSS Number Example CVSS v3 score of detected vulnerability.
CriminalIP.IP.raw Unknown Full raw response from CriminalIP API.

Example

!criminal-ip-ip-report ip=8.8.8.8

criminal-ip-check-malicious-ip


Determines whether an IP is malicious or safe through CriminalIP Asset Search.

Base Command

criminal-ip-check-malicious-ip

Input

Argument Name Description Required
ip IP Address to check. Required

Context Output

Path Type Description
CriminalIP.Mal_IP.ip String Queried IP address.
CriminalIP.Mal_IP.malicious Boolean Whether the IP was detected as malicious.
CriminalIP.Mal_IP.real_ip_list Unknown List of real IPs if protected IP was detected.
CriminalIP.Mal_IP.raw Unknown Full raw response.

Example

!criminal-ip-check-malicious-ip ip=192.168.1.1

criminal-ip-check-last-scan-date


Checks if the domain has been scanned within the last 7 days.

Base Command

criminal-ip-check-last-scan-date

Input

Argument Name Description Required
domain The domain to check last scan date for. Required

Context Output

Path Type Description
CriminalIP.Scan_Date.domain String Queried Domain.
CriminalIP.Scan_Date.scan_id String The last scan ID of the domain.
CriminalIP.Scan_Date.scanned Boolean Whether the domain was scanned within the last 7 days.
CriminalIP.Scan_Date.scan_date String The last scan date in ISO format.
CriminalIP.Scan_Date.raw Unknown Full raw response from CriminalIP API.

Example

!criminal-ip-check-last-scan-date domain=example.com

criminal-ip-domain-quick-scan


Performs a Domain Quick Scan using CriminalIP’s API.

Base Command

criminal-ip-domain-quick-scan

Input

Argument Name Description Required
domain The domain to perform Quick Scan. Required

Context Output

Path Type Description
CriminalIP.Domain_Quick.domain String Queried Domain.
CriminalIP.Domain_Quick.reg_dtime String Domain registration time.
CriminalIP.Domain_Quick.result String Quick scan result string.
CriminalIP.Domain_Quick.type String Domain type classification.
CriminalIP.Domain_Quick.raw Unknown Full raw response.

Example

!criminal-ip-domain-quick-scan domain=example.com

criminal-ip-domain-lite-scan


Initiates a Domain Lite Scan and returns a scan_id.

Base Command

criminal-ip-domain-lite-scan

Input

Argument Name Description Required
domain The domain to perform Lite Scan. Required

Context Output

Path Type Description
CriminalIP.Domain_Lite.scan_id String Scan ID returned for Lite Scan.
CriminalIP.Domain_Lite.raw Unknown Full raw response.

Example

!criminal-ip-domain-lite-scan domain=example.com

criminal-ip-domain-lite-scan-status


Checks the progress of the Lite Scan.

Base Command

criminal-ip-domain-lite-scan-status

Input

Argument Name Description Required
scan_id The scan_id whose Lite Scan progress to check. Required

Context Output

Path Type Description
CriminalIP.Domain_Lite_Status.status String Lite Scan status.
CriminalIP.Domain_Lite_Status.scan_percentage Number Scan percentage progress.
CriminalIP.Domain_Lite_Status.raw Unknown Full raw response.

Example

!criminal-ip-domain-lite-scan-status scan_id=abc123def456

criminal-ip-domain-lite-scan-result


Returns the Lite Scan results for the given scan_id.

Base Command

criminal-ip-domain-lite-scan-result

Input

Argument Name Description Required
scan_id The scan_id whose Lite Scan result to fetch. Required

Context Output

Path Type Description
CriminalIP.Domain_Lite_Result.domain String Queried Domain.
CriminalIP.Domain_Lite_Result.created String Domain creation date.
CriminalIP.Domain_Lite_Result.registrar String Domain registrar.
CriminalIP.Domain_Lite_Result.score String Domain risk score.
CriminalIP.Domain_Lite_Result.report_time String Report generation time.
CriminalIP.Domain_Lite_Result.phishing_prob Number Phishing probability.
CriminalIP.Domain_Lite_Result.dga_score Number DGA score.
CriminalIP.Domain_Lite_Result.abuse_critical Number Critical abuse record count.
CriminalIP.Domain_Lite_Result.abuse_dangerous Number Dangerous abuse record count.
CriminalIP.Domain_Lite_Result.a_records String A records resolved.
CriminalIP.Domain_Lite_Result.ns_records String NS records resolved.
CriminalIP.Domain_Lite_Result.mapped_ips String Mapped IP list.
CriminalIP.Domain_Lite_Result.raw Unknown Full raw response.

Example

!criminal-ip-domain-lite-scan-result scan_id=abc123def456

criminal-ip-domain-full-scan


Initiates a Domain Full Scan and returns a scan_id.

Base Command

criminal-ip-domain-full-scan

Input

Argument Name Description Required
domain The domain to perform Full Scan. Required

Context Output

Path Type Description
CriminalIP.Full_Scan.scan_id String Scan ID returned for Full Scan.
CriminalIP.Full_Scan.raw Unknown Full raw response.

Example

!criminal-ip-domain-full-scan domain=example.com

criminal-ip-domain-full-scan-status


Checks the progress of the Full Scan.

Base Command

criminal-ip-domain-full-scan-status

Input

Argument Name Description Required
scan_id The scan_id whose Full Scan status to check. Required

Context Output

Path Type Description
CriminalIP.Full_Scan_Status.status String Full Scan status.
CriminalIP.Full_Scan_Status.scan_percentage Number Scan percentage progress.
CriminalIP.Full_Scan_Status.raw Unknown Full raw response.

Example

!criminal-ip-domain-full-scan-status scan_id=xyz789abc123

criminal-ip-domain-full-scan-result


Returns the Full Scan results for the given scan_id.

Base Command

criminal-ip-domain-full-scan-result

Input

Argument Name Description Required
scan_id The scan_id whose Full Scan result to fetch. Required

Context Output

Path Type Description
CriminalIP.Full_Scan_Result.domain String Queried Domain.
CriminalIP.Full_Scan_Result.created String Domain creation date.
CriminalIP.Full_Scan_Result.registrar String Domain registrar.
CriminalIP.Full_Scan_Result.score String Domain risk score.
CriminalIP.Full_Scan_Result.report_time String Report generation time.
CriminalIP.Full_Scan_Result.phishing_prob Number Phishing probability.
CriminalIP.Full_Scan_Result.dga_score Number DGA score.
CriminalIP.Full_Scan_Result.punycode Boolean Whether punycode detected.
CriminalIP.Full_Scan_Result.fake_https Boolean Whether fake HTTPS detected.
CriminalIP.Full_Scan_Result.abuse_critical Number Critical abuse record count.
CriminalIP.Full_Scan_Result.abuse_dangerous Number Dangerous abuse record count.
CriminalIP.Full_Scan_Result.cert_valid_to String Certificate valid until date.
CriminalIP.Full_Scan_Result.connected_ips String Connected IP list.
CriminalIP.Full_Scan_Result.ssl_vulns String SSL vulnerabilities detected.
CriminalIP.Full_Scan_Result.raw Unknown Full raw response.

Example

!criminal-ip-domain-full-scan-result scan_id=xyz789abc123

criminal-ip-domain-full-scan-make-email-body


Builds an email body summarizing notable findings from a completed Full Scan.

Base Command

criminal-ip-domain-full-scan-make-email-body

Input

Argument Name Description Required
scan_id The scan_id of the completed Full Scan. Required
domain The domain of the completed Full Scan. Required

Context Output

Path Type Description
CriminalIP.Email_Body.domain String Domain analyzed.
CriminalIP.Email_Body.scan_id String Scan ID used for generating the email body.
CriminalIP.Email_Body.domain_score String Domain score.
CriminalIP.Email_Body.phishing_prob Number Phishing probability.
CriminalIP.Email_Body.dga_score Number DGA score.
CriminalIP.Email_Body.registrar String Domain registrar.
CriminalIP.Email_Body.created String Domain creation date.
CriminalIP.Email_Body.report_time String Report generation time.
CriminalIP.Email_Body.abuse_critical Number Critical abuse record count.
CriminalIP.Email_Body.abuse_dangerous Number Dangerous abuse record count.
CriminalIP.Email_Body.fake_https Boolean Whether fake HTTPS detected.
CriminalIP.Email_Body.punycode Boolean Whether punycode detected.
CriminalIP.Email_Body.cert_valid_to String Certificate expiration date.
CriminalIP.Email_Body.connected_ips String Connected IP addresses (comma-separated).
CriminalIP.Email_Body.ssl_vulns String SSL vulnerabilities detected.
CriminalIP.Email_Body.readable_output String Pre-formatted Full Scan report (email-ready).
CriminalIP.Email_Body.raw Unknown Full raw response from Criminal IP API.

Example

!criminal-ip-domain-full-scan-make-email-body scan_id=xyz789abc123 domain=example.com

criminal-ip-micro-asm


Performs a micro ASM-style summary for a domain with a completed Full Scan.

Base Command

criminal-ip-micro-asm

Input

Argument Name Description Required
scan_id The scan_id of the completed Full Scan. Required
domain The domain of the completed Full Scan. Required

Context Output

Path Type Description
CriminalIP.Micro_ASM.domain String Domain analyzed.
CriminalIP.Micro_ASM.scan_id String Scan ID used for Micro ASM.
CriminalIP.Micro_ASM.domain_score String Domain score.
CriminalIP.Micro_ASM.phishing_prob Number Phishing probability.
CriminalIP.Micro_ASM.dga_score Number DGA score.
CriminalIP.Micro_ASM.registrar String Domain registrar.
CriminalIP.Micro_ASM.created String Domain creation date.
CriminalIP.Micro_ASM.report_time String Report generation time.
CriminalIP.Micro_ASM.abuse_critical Number Critical abuse record count.
CriminalIP.Micro_ASM.abuse_dangerous Number Dangerous abuse record count.
CriminalIP.Micro_ASM.fake_https Boolean Whether fake HTTPS detected.
CriminalIP.Micro_ASM.punycode Boolean Whether punycode detected.
CriminalIP.Micro_ASM.cert_valid_to String Certificate expiration date.
CriminalIP.Micro_ASM.connected_ips String Connected IP addresses (comma-separated).
CriminalIP.Micro_ASM.ssl_vulns String SSL vulnerabilities detected.
CriminalIP.Micro_ASM.readable_output String Pre-formatted Micro ASM report (email-ready).
CriminalIP.Micro_ASM.raw Unknown Full raw response from CriminalIP API.

Example

!criminal-ip-micro-asm scan_id=xyz789abc123 domain=example.com

Resources

Configuration parameters

  • credentials
  • url — Server URL (required)
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • request_timeout — Request timeout (seconds)

Commands (12)

  • criminal-ip-check-last-scan-date

    Checks if the domain has been scanned within the last 7 days.

  • criminal-ip-check-malicious-ip

    Determines whether an IP is malicious or safe through CriminalIP Asset Search.

  • criminal-ip-domain-full-scan

    Initiates a Domain Full Scan and returns a scan_id.

  • criminal-ip-domain-full-scan-make-email-body

    Builds an email body summarizing notable findings from a completed Full Scan.

  • criminal-ip-domain-full-scan-result

    Returns the Full Scan results for the given scan_id.

  • criminal-ip-domain-full-scan-status

    Checks the progress of the Full Scan.

  • criminal-ip-domain-lite-scan

    Initiates a Domain Lite Scan and returns a scan_id.

  • criminal-ip-domain-lite-scan-result

    Returns the Lite Scan results for the given scan_id.

  • criminal-ip-domain-lite-scan-status

    Checks the progress of the Lite Scan.

  • criminal-ip-domain-quick-scan

    Performs a Domain Quick Scan using CriminalIP's API.

  • criminal-ip-ip-report

    Provides detailed information about an IP address using Criminal IP's API.

  • criminal-ip-micro-asm

    Performs a micro ASM-style summary for a domain with a completed Full Scan.

import demistomock as demisto  # noqa: F401
from CommonServerPython import *  # noqa: F401  # pylint: disable=import-error
from CommonServerUserPython import *  # noqa: F401  # pylint: disable=import-error
from datetime import datetime, UTC
import dateparser
from typing import Any, cast


class Client(BaseClient):
    """
    Criminal IP API Client using XSOAR BaseClient.
    """

    def __init__(
        self,
        base_url: str,
        verify: bool,
        proxy: bool,
        headers: dict[str, str],
        timeout: float | None = None,
    ):
        super().__init__(
            base_url=base_url.rstrip("/") if base_url else "",
            verify=verify,
            proxy=proxy,
            headers=headers,
        )
        self.timeout: float = float(timeout) if timeout is not None else 0.0

    def ip_lookup(self, ip: str) -> dict:
        return self._http_request(
            "GET",
            "/v1/asset/ip/report",
            params={"ip": ip},
            timeout=self.timeout,
        )

    def domain_quick_scan(self, domain: str) -> dict:
        return self._http_request(
            "GET",
            "/v1/domain/quick/hash/view",
            params={"domain": domain},
            timeout=self.timeout,
        )

    def domain_lite_scan(self, domain: str) -> dict:
        return self._http_request(
            "GET",
            "/v1/domain/lite/scan",
            params={"query": domain},
            timeout=self.timeout,
        )

    def domain_lite_scan_status(self, scan_id: str) -> dict:
        return self._http_request(
            "GET",
            "/v1/domain/lite/progress",
            params={"scan_id": scan_id},
            timeout=self.timeout,
        )

    def domain_lite_scan_result(self, scan_id: str) -> dict:
        return self._http_request(
            "GET",
            f"/v1/domain/lite/report/{scan_id}",
            timeout=self.timeout,
        )

    def domain_full_scan(self, domain: str) -> dict:
        return self._http_request(
            "POST",
            "/v1/domain/scan",
            files={"query": (None, domain)},
            timeout=self.timeout,
        )

    def domain_full_scan_status(self, scan_id: str) -> dict:
        return self._http_request(
            "GET",
            f"/v1/domain/status/{scan_id}",
            timeout=self.timeout,
        )

    def domain_full_scan_result(self, scan_id: str) -> dict:
        return self._http_request(
            "GET",
            f"/v2/domain/report/{scan_id}",
            timeout=self.timeout,
        )

    def domain_reports(self, domain: str) -> dict:
        return self._http_request(
            "GET",
            "/v1/domain/reports",
            params={"query": domain, "offset": 0},
            timeout=self.timeout,
        )


def validate_response(raw: dict, prefix: str, scan_id: str | None = None, domain: str | None = None):
    status = raw.get("status")

    if status and status != 200:
        msg = raw.get("message") or raw.get("error") or "Unknown error"
        if scan_id:
            return_error(f"CriminalIP Error: {msg} (scan_id={scan_id}). Details: {raw}")
        elif domain:
            return_error(f"CriminalIP Error: {msg} (domain={domain}). Details: {raw}")
        else:
            return_error(f"CriminalIP Error: {msg}. Details: {raw}")

    if status == 200 and not raw.get("data"):
        return CommandResults(
            readable_output=tableToMarkdown(f"{prefix} - No data available", [{}]),
            outputs_prefix=prefix,
            outputs={},
            raw_response=raw,
        )

    return None


def _wrap_simple_output(prefix: str, raw: dict, key_field: str = "") -> CommandResults:
    if not raw:
        return_error(f"CriminalIP Error: Empty response for {prefix}.")

    if isinstance(raw, dict):
        if "error" in raw or "errors" in raw:
            err_msg = raw.get("error") or raw.get("errors")
            return_error(f"CriminalIP Error: {err_msg}. Details: {raw}")
        if "message" in raw and str(raw.get("message")).lower() not in ("ok", "success", "api success"):
            err_msg = raw.get("message")
            return_error(f"CriminalIP Error: {err_msg}. Details: {raw}")

    status_val = None
    for status_key in ("status", "code", "status_code", "http_status"):
        if status_key in raw:
            status_val = raw.get(status_key)
            break
    if status_val is not None:
        try:
            s_int = int(status_val)
            if s_int != 200:
                return_error(f"CriminalIP Error: HTTP status {s_int} returned for {prefix}. Details: {raw}")
        except Exception:
            if str(status_val).lower() not in ("200", "ok", "success"):
                return_error(f"CriminalIP Error: Status '{status_val}' returned for {prefix}. Details: {raw}")

    data_container = raw.get("data") if isinstance(raw.get("data"), dict) else raw
    flat: dict[str, Any] = {}

    def extract_scan_id() -> Any:
        if isinstance(data_container, dict):
            if "scan_id" in data_container:
                return data_container.get("scan_id")
            if "data" in data_container and isinstance(data_container["data"], dict):
                return data_container["data"].get("scan_id")
        return raw.get("scan_id")

    def extract_progress() -> Any:
        for key in ("scan_percentage", "progress", "percentage"):
            if isinstance(data_container, dict) and key in data_container:
                return data_container.get(key)
            if key in raw:
                return raw.get(key)
        state = (raw.get("state") or (data_container or {}).get("state") or "").lower()
        if state in {"done", "completed", "complete", "finished", "success"}:
            return 100
        if state in {"running", "in_progress", "scanning", "queued"}:
            return 0
        return None

    if prefix in {"CriminalIP.Full_Scan", "CriminalIP.Domain_Lite"}:
        scan_id = extract_scan_id()
        if not scan_id:
            return_error("CriminalIP Error: No scan_id returned")
        flat["scan_id"] = scan_id
        flat["data"] = {"scan_id": scan_id}

    elif prefix in {"CriminalIP.Full_Scan_Status", "CriminalIP.Domain_Lite_Status"}:
        pct = extract_progress()
        if pct is None:
            return_error("CriminalIP Error: Could not extract scan progress")
        flat["scan_percentage"] = pct
        flat["data"] = {"scan_percentage": pct}
        if "status" in raw:
            flat["status"] = raw["status"]
        if "state" in raw:
            flat["state"] = raw["state"]

    elif prefix == "CriminalIP.Domain_Quick":
        data_section = raw.get("data") or {}
        flat = {
            "domain": data_section.get("domain"),
            "reg_dtime": data_section.get("reg_dtime"),
            "result": data_section.get("result"),
            "type": data_section.get("type"),
        }
        readable = tableToMarkdown(
            "CriminalIP - Domain Quick Scan (Risk Result)",
            [
                {
                    "Domain": flat.get("domain", "N/A"),
                    "Reg Date": flat.get("reg_dtime", "N/A"),
                    "Risk Result": flat.get("result", "N/A"),
                    "Type": flat.get("type", "N/A"),
                }
            ],
        )
        return CommandResults(
            readable_output=readable,
            outputs_prefix=prefix,
            outputs_key_field="domain",
            outputs=flat,
            raw_response=raw,
        )

    elif prefix == "CriminalIP.Domain_Lite_Result":
        domain_info = (data_container or {}).get("main_domain_info") or {}
        dns_record = (data_container or {}).get("dns_record") or {}
        summary = (data_container or {}).get("summary") or {}
        mapped_ips = (data_container or {}).get("mapped_ip") or []
        if isinstance(mapped_ips, dict):
            mapped_ips = [mapped_ips]

        def _join_safe(lst):
            if not lst:
                return "N/A"
            return ", ".join(lst)

        a_records_list: list[str] = []
        try:
            a_records_list = (dns_record.get("dns_record_type_a") or {}).get("ipv4", []) or []
        except Exception:
            a_records_list = []

        ns_records_list = dns_record.get("dns_record_type_ns", []) or []

        flat = {
            "domain": domain_info.get("main_domain") or "N/A",
            "created": domain_info.get("domain_created") or "N/A",
            "registrar": domain_info.get("domain_registrar") or "N/A",
            "score": data_container.get("domain_score") if isinstance(data_container, dict) else "N/A",
            "report_time": raw.get("report_time")
            or (data_container.get("report_time") if isinstance(data_container, dict) else "N/A"),
            "phishing_prob": summary.get("url_phishing_prob") if summary.get("url_phishing_prob") is not None else "N/A",
            "dga_score": summary.get("dga_score") if summary.get("dga_score") is not None else "N/A",
            "abuse_critical": (summary.get("abuse_record") or {}).get("critical", 0),
            "abuse_dangerous": (summary.get("abuse_record") or {}).get("dangerous", 0),
            "a_records": _join_safe(a_records_list[:5]),
            "ns_records": _join_safe(ns_records_list[:3]),
            "mapped_ips": _join_safe([f"{i.get('ip')}({i.get('score')})" for i in mapped_ips[:5]]),
        }

        readable = tableToMarkdown("CriminalIP - Domain Lite Scan Result (Summary)", [flat])
        return CommandResults(
            readable_output=readable,
            outputs_prefix=prefix,
            outputs_key_field="domain",
            outputs=flat,
            raw_response=raw,
        )

    elif prefix == "CriminalIP.Full_Scan_Result":
        data_section = (data_container or {}) or {}
        domain_info = data_section.get("main_domain_info") or {}
        summary = data_section.get("summary") or {}
        certs = data_section.get("certificates") or []
        connected_ips = data_section.get("connected_ip_info") or []
        ssl_detail = data_section.get("ssl_detail") or {}

        def _join_connected(ips):
            if not ips:
                return "N/A"
            return ", ".join([f"{i.get('ip')}({i.get('score')},{i.get('country')})" for i in ips[:5]])

        flat = {
            "domain": domain_info.get("main_domain") or "N/A",
            "created": domain_info.get("domain_created") or "N/A",
            "registrar": domain_info.get("domain_registrar") or "N/A",
            "score": (domain_info.get("domain_score") or {}).get("score") or "N/A",
            "report_time": raw.get("report_time")
            or (data_section.get("report_time") if isinstance(data_section, dict) else "N/A"),
            "phishing_prob": summary.get("url_phishing_prob") if summary.get("url_phishing_prob") is not None else "N/A",
            "dga_score": summary.get("dga_score") if summary.get("dga_score") is not None else "N/A",
            "punycode": summary.get("punycode") if summary.get("punycode") is not None else "N/A",
            "fake_https": summary.get("fake_https_url") if summary.get("fake_https_url") is not None else "N/A",
            "abuse_critical": (summary.get("abuse_record") or {}).get("critical", 0),
            "abuse_dangerous": (summary.get("abuse_record") or {}).get("dangerous", 0),
            "cert_valid_to": certs[0].get("valid_to") if certs else "N/A",
            "connected_ips": _join_connected(connected_ips),
            "score_percentage": (domain_info.get("domain_score") or {}).get("score_percentage") or "N/A",
            "ssl_vulns": ", ".join([k for k, v in (ssl_detail.get("vulnerable") or {}).items() if v]) or "None",
        }

        readable = tableToMarkdown("CriminalIP - Full Scan Result (Summary)", [flat])
        return CommandResults(
            readable_output=readable,
            outputs_prefix=prefix,
            outputs_key_field="domain",
            outputs=flat,
            raw_response=raw,
        )

    else:
        if isinstance(data_container, dict) and data_container:
            flat = data_container
        else:
            flat = raw or {"info": "No data available"}

    return CommandResults(
        readable_output=tableToMarkdown(prefix, [flat]),
        outputs_prefix=prefix,
        outputs_key_field=key_field or "",
        outputs=flat,
        raw_response=raw,
    )


def get_ip_report(client: Client, args: dict[str, Any]) -> CommandResults:
    ip = args.get("ip")
    if not ip:
        return_error("ip argument is required")
    ip = cast(str, ip)

    raw = client.ip_lookup(ip)
    if not raw or "error" in raw:
        return_error(f"CriminalIP Error: No IP report found for {ip}")

    first_whois = (raw.get("whois", {}).get("data") or [{}])[0]
    first_hostname = (raw.get("hostname", {}).get("data") or [{}])[0]
    first_port = (raw.get("port", {}).get("data") or [{}])[0]
    first_vuln = (raw.get("vulnerability", {}).get("data") or [{}])[0]

    summary = {
        "IP": raw.get("ip"),
        "InboundScore": raw.get("score", {}).get("inbound"),
        "OutboundScore": raw.get("score", {}).get("outbound"),
        "Issues": ", ".join([k for k, v in (raw.get("issues") or {}).items() if v]) or "None",
        "ProtectedIPs": raw.get("protected_ip", {}).get("count", 0),
        "RelatedDomains": raw.get("domain", {}).get("count", 0),
        "ASN": first_whois.get("as_no"),
        "ASName": first_whois.get("as_name"),
        "Org": first_whois.get("org_name"),
        "Country": first_whois.get("org_country_code"),
        "Hostname": first_hostname.get("domain_name_full"),
        "OpenPorts": len(raw.get("port", {}).get("data", [])),
        "ObservedPort": first_port.get("open_port_no"),
        "ObservedService": first_port.get("app_name"),
        "Vulnerabilities": len(raw.get("vulnerability", {}).get("data", [])),
        "ObservedCVE": first_vuln.get("cve_id"),
        "ObservedCVSS": first_vuln.get("cvssv3_score"),
    }

    return CommandResults(
        readable_output=tableToMarkdown("CriminalIP - IP Report (Extended Summary)", [summary]),
        outputs_prefix="CriminalIP.IP",
        outputs_key_field="ip",
        outputs=summary,
        raw_response=raw,
    )


def check_malicious_ip(client: Client, args: dict[str, Any]) -> CommandResults:
    ip = args.get("ip")
    if not ip:
        return_error("ip argument is required")
    ip = cast(str, ip)

    data = client.ip_lookup(ip)
    if not data or "error" in data:
        return_error(f"CriminalIP Error: No data returned for IP {ip}")

    malicious = False
    real_ip_list: list[dict] = []

    score = data.get("score", {})
    if score.get("inbound") in ("Dangerous", "Critical") or score.get("outbound") in ("Dangerous", "Critical"):
        malicious = True

    protected = data.get("protected_ip", {})
    if protected.get("count", 0) > 0:
        malicious = True
        real_ip_list = protected.get("data", [])

    if any((data.get("issues") or {}).values()):
        malicious = True

    outputs = {
        "ip": ip,
        "malicious": malicious,
        "real_ip_list": real_ip_list,
        "raw": data,
    }

    real_ips_str = ", ".join([item.get("ip_address", "") for item in real_ip_list]) if real_ip_list else "None"

    readable_output = f"Malicious: {malicious}\n" + tableToMarkdown(
        "CriminalIP - Malicious IP Check",
        [
            {
                "IP": ip,
                "Malicious": malicious,
                "RealIPs": real_ips_str,
            }
        ],
    )

    return CommandResults(
        readable_output=readable_output,
        outputs_prefix="CriminalIP.Mal_IP",
        outputs_key_field="ip",
        outputs=outputs,
        raw_response=data,
    )


def _to_aware_utc(dt: datetime | None) -> datetime | None:
    if not dt:
        return None
    if dt.tzinfo is None:
        return dt.replace(tzinfo=UTC)
    return dt.astimezone(UTC)


def check_last_scan_date(client: Client, args: dict[str, Any]) -> CommandResults:
    domain = args.get("domain")
    if not domain:
        return_error("domain argument is required")
    domain = cast(str, domain)

    raw = client.domain_reports(domain)
    if not raw or "error" in raw:
        return_error(f"CriminalIP Error: No scan reports found for {domain}")

    reports = (raw.get("data") or {}).get("reports", [])
    if not reports:
        return CommandResults(
            readable_output=tableToMarkdown(
                "CriminalIP - Last Scan Date Check",
                [{"Domain": domain, "Scan ID": "N/A", "Scan Date": "N/A", "Scanned (within 7d)": False}],
            ),
            outputs_prefix="CriminalIP.Scan_Date",
            outputs_key_field="scan_id",
            outputs={"last_scan_date": None},
            raw_response=raw,
        )
    report = reports[0]
    reg_dt = _to_aware_utc(dateparser.parse(report.get("reg_dtime", "")))
    now = datetime.now(UTC)
    scanned_bool = bool(reg_dt and (now - reg_dt).days <= 7)

    outputs = {
        "domain": domain,
        "scan_id": report.get("scan_id"),
        "scan_date": str(reg_dt) if reg_dt else "",
        "scanned": scanned_bool,
        "raw": raw,
    }

    readable = tableToMarkdown(
        "CriminalIP - Last Scan Date Check",
        [
            {
                "Domain": domain,
                "Scan ID": report.get("scan_id"),
                "Scan Date": str(reg_dt) if reg_dt else "N/A",
                "Scanned (within 7d)": scanned_bool,
            }
        ],
    )

    return CommandResults(
        readable_output=readable,
        outputs_prefix="CriminalIP.Scan_Date",
        outputs_key_field="scan_id",
        outputs=outputs,
        raw_response=raw,
    )


def domain_quick_scan(client: Client, args: dict[str, Any]) -> CommandResults:
    domain = args.get("domain")
    if not domain:
        return_error("domain argument is required")
    domain = cast(str, domain)

    raw = client.domain_quick_scan(domain)
    return _wrap_simple_output("CriminalIP.Domain_Quick", raw, key_field="domain")


def domain_lite_scan(client: Client, args: dict[str, Any]) -> CommandResults:
    domain = args.get("domain")
    if not domain:
        return_error("domain argument is required")
    domain = cast(str, domain)

    raw = client.domain_lite_scan(domain)

    scan_id = (raw.get("data") or {}).get("scan_id")
    if not scan_id:
        return_error(f"CriminalIP Error: No scan_id returned. Details: {raw}")

    outputs = {"domain": domain, "scan_id": scan_id}

    readable = tableToMarkdown(
        "CriminalIP - Domain Lite Scan Started",
        [{"Domain": domain, "Scan ID": scan_id}],
        headers=["Domain", "Scan ID"],
        removeNull=True,
    )

    return CommandResults(
        readable_output=readable,
        outputs_prefix="CriminalIP.Domain_Lite",
        outputs_key_field="scan_id",
        outputs=outputs,
        raw_response=raw,
    )


def domain_lite_scan_status(client: Client, args: dict[str, Any]) -> CommandResults:
    scan_id = args.get("scan_id")
    if not scan_id:
        return_error("scan_id argument is required")
    scan_id = cast(str, scan_id)

    raw = client.domain_lite_scan_status(scan_id)
    if not raw or not raw.get("data"):
        return_error(f"CriminalIP Error: No status found for scan_id={scan_id}")
    return _wrap_simple_output("CriminalIP.Domain_Lite_Status", raw)


def domain_lite_scan_result(client: Client, args: dict[str, Any]) -> CommandResults:
    scan_id = args.get("scan_id")
    if not scan_id:
        return_error("scan_id argument is required")
    scan_id = cast(str, scan_id)

    raw = client.domain_lite_scan_result(scan_id)
    if not raw or not raw.get("data"):
        return_error(f"CriminalIP Error: No result found for scan_id={scan_id}")
    return _wrap_simple_output("CriminalIP.Domain_Lite_Result", raw, key_field="domain")


def domain_full_scan(client: Client, args: dict[str, Any]) -> CommandResults:
    domain = args.get("domain")
    if not domain:
        return_error("domain argument is required")
    domain = cast(str, domain)

    raw = client.domain_full_scan(domain)

    scan_id = (raw.get("data") or {}).get("scan_id")
    if not scan_id:
        return_error(f"CriminalIP Error: No scan_id returned. Details: {raw}")

    outputs = {"scan_id": scan_id}

    readable = tableToMarkdown(
        "CriminalIP - Full Scan Started",
        [{"Domain": domain}, {"Scan ID": scan_id}],
        headers=[],
        removeNull=True,
    )

    return CommandResults(
        readable_output=readable,
        outputs_prefix="CriminalIP.Full_Scan",
        outputs_key_field="scan_id",
        outputs=outputs,
        raw_response=raw,
    )


def domain_full_scan_status(client: Client, args: dict[str, Any]) -> CommandResults:
    scan_id = args.get("scan_id")
    if not scan_id:
        return_error("scan_id argument is required")
    scan_id = cast(str, scan_id)

    raw = client.domain_full_scan_status(scan_id)
    if not raw or not raw.get("data"):
        return_error(f"CriminalIP Error: No status found for scan_id={scan_id}")
    return _wrap_simple_output("CriminalIP.Full_Scan_Status", raw)


def domain_full_scan_result(client: Client, args: dict[str, Any]) -> CommandResults:
    scan_id = args.get("scan_id")
    if not scan_id:
        return_error("scan_id argument is required")
    scan_id = cast(str, scan_id)

    raw = client.domain_full_scan_result(scan_id)
    if not raw or not raw.get("data"):
        return_error(f"CriminalIP Error: No result found for scan_id={scan_id}")
    return _wrap_simple_output("CriminalIP.Full_Scan_Result", raw, key_field="domain")


def make_email_body(client: Client, args: dict[str, Any]) -> CommandResults:
    domain, scan_id = args.get("domain"), args.get("scan_id")
    if not scan_id or not domain:
        return_error("Both scan_id and domain are required")

    scan_id = cast(str, scan_id)
    domain = cast(str, domain)

    raw = client.domain_full_scan_result(scan_id)
    if not raw or "error" in raw:
        return_error(f"CriminalIP Error: No scan result found for scan_id={scan_id}")

    data = raw.get("data", {}) or {}
    domain_info = data.get("main_domain_info", {}) or {}
    summary = data.get("summary", {}) or {}
    certs = data.get("certificates", []) or []
    ssl_detail = data.get("ssl_detail", {}) or {}
    connected_ips = data.get("connected_ip_info", []) or []

    abuse_record = summary.get("abuse_record", {}) or {}
    abuse_critical = abuse_record.get("critical", 0)
    abuse_dangerous = abuse_record.get("dangerous", 0)
    cert_valid_to = certs[0].get("valid_to") if certs else "N/A"

    connected_ips_str = (
        ", ".join([f"{ip.get('ip')}({ip.get('score')},{ip.get('country')})" for ip in connected_ips]) if connected_ips else "N/A"
    )
    ssl_vulns = ", ".join([k for k, v in (ssl_detail.get("vulnerable") or {}).items() if v]) or "None"
    domain_score = domain_info.get("domain_score") or {}

    flat = {
        "Domain": domain_info.get("main_domain") or domain,
        "Scan ID": scan_id,
        "Domain Score": domain_score.get("score", "N/A"),
        "Score %": domain_score.get("score_percentage", "N/A"),
        "Certificate Valid To": cert_valid_to,
        "Connected IPs": connected_ips_str,
        "Phishing Probability": summary.get("url_phishing_prob", "N/A"),
        "DGA Score": summary.get("dga_score", "N/A"),
        "Registrar": domain_info.get("domain_registrar", "N/A"),
        "Created": domain_info.get("domain_created", "N/A"),
        "Report Time": data.get("report_time", "N/A"),
        "Abuse Critical": abuse_critical,
        "Abuse Dangerous": abuse_dangerous,
        "Fake HTTPS": summary.get("fake_https_url", False),
        "Punycode": summary.get("punycode", False),
        "SSL Vulns": ssl_vulns,
    }

    readable = tableToMarkdown("CriminalIP - Full Scan Report", [flat])

    return CommandResults(
        readable_output=readable,
        outputs_prefix="CriminalIP.Email_Body",
        outputs_key_field="scan_id",
        outputs={
            "domain": domain,
            "scan_id": scan_id,
            "domain_score": domain_score.get("score"),
            "phishing_prob": summary.get("url_phishing_prob"),
            "dga_score": summary.get("dga_score"),
            "registrar": domain_info.get("domain_registrar"),
            "created": domain_info.get("domain_created"),
            "report_time": data.get("report_time"),
            "abuse_critical": abuse_critical,
            "abuse_dangerous": abuse_dangerous,
            "fake_https": summary.get("fake_https_url"),
            "punycode": summary.get("punycode"),
            "cert_valid_to": cert_valid_to,
            "connected_ips": connected_ips_str,
            "ssl_vulns": ssl_vulns,
            "readable_output": readable,
            "raw": raw,
        },
        raw_response=raw,
    )


def micro_asm(client: Client, args: dict[str, Any]) -> CommandResults:
    domain, scan_id = args.get("domain"), args.get("scan_id")
    if not scan_id or not domain:
        return_error("Both scan_id and domain are required")

    scan_id = cast(str, scan_id)
    domain = cast(str, domain)

    raw = client.domain_full_scan_result(scan_id)
    if not raw or "error" in raw:
        return_error(f"CriminalIP Error: No scan result found for scan_id={scan_id}")

    data = raw.get("data", {}) or {}
    domain_info = data.get("main_domain_info", {}) or {}
    summary = data.get("summary", {}) or {}
    certs = data.get("certificates", []) or []
    ssl_detail = data.get("ssl_detail", {}) or {}
    connected_ips = data.get("connected_ip_info", []) or []

    abuse_record = summary.get("abuse_record", {}) or {}
    abuse_critical = abuse_record.get("critical", 0)
    abuse_dangerous = abuse_record.get("dangerous", 0)
    cert_valid_to = certs[0].get("valid_to") if certs else "N/A"

    connected_ips_str = (
        ", ".join([f"{ip.get('ip')}({ip.get('score')},{ip.get('country')})" for ip in connected_ips]) if connected_ips else "N/A"
    )
    ssl_vulns = ", ".join([k for k, v in (ssl_detail.get("vulnerable") or {}).items() if v]) or "None"
    domain_score = domain_info.get("domain_score") or {}

    flat = {
        "Domain": domain_info.get("main_domain") or domain,
        "Scan ID": scan_id,
        "Domain Score": domain_score.get("score", "N/A"),
        "Score %": domain_score.get("score_percentage", "N/A"),
        "Certificate Valid To": cert_valid_to,
        "Connected IPs": connected_ips_str,
        "Phishing Probability": summary.get("url_phishing_prob", "N/A"),
        "DGA Score": summary.get("dga_score", "N/A"),
        "Registrar": domain_info.get("domain_registrar", "N/A"),
        "Created": domain_info.get("domain_created", "N/A"),
        "Report Time": data.get("report_time", "N/A"),
        "Abuse Critical": abuse_critical,
        "Abuse Dangerous": abuse_dangerous,
        "Fake HTTPS": summary.get("fake_https_url", False),
        "Punycode": summary.get("punycode", False),
        "SSL Vulns": ssl_vulns,
    }

    readable = tableToMarkdown("CriminalIP - Micro ASM Report", [flat])

    return CommandResults(
        readable_output=readable,
        outputs_prefix="CriminalIP.Micro_ASM",
        outputs_key_field="scan_id",
        outputs={
            "domain": domain,
            "scan_id": scan_id,
            "domain_score": domain_score.get("score"),
            "phishing_prob": summary.get("url_phishing_prob"),
            "dga_score": summary.get("dga_score"),
            "registrar": domain_info.get("domain_registrar"),
            "created": domain_info.get("domain_created"),
            "report_time": data.get("report_time"),
            "abuse_critical": abuse_critical,
            "abuse_dangerous": abuse_dangerous,
            "fake_https": summary.get("fake_https_url"),
            "punycode": summary.get("punycode"),
            "cert_valid_to": cert_valid_to,
            "connected_ips": connected_ips_str,
            "ssl_vulns": ssl_vulns,
            "readable_output": readable,
            "raw": raw,
        },
        raw_response=raw,
    )


def test_module(client: Client) -> str:
    try:
        client.ip_lookup("8.8.8.8")
        return "ok"
    except Exception as e:
        return f"Test failed: {str(e)}"


def main() -> None:
    params = demisto.params()
    command = demisto.command()

    base_url = params.get("url")
    creds = params.get("credentials") or {}
    api_key = creds.get("password") or params.get("apikey") or ""
    verify = not params.get("insecure", False)
    proxy = params.get("proxy", False)
    timeout = float(params.get("request_timeout") or 30)

    headers = {"x-api-key": api_key} if api_key else {}

    client = Client(base_url=base_url, verify=verify, proxy=proxy, headers=headers, timeout=timeout)

    commands: dict[str, Any] = {
        "test-module": lambda: test_module(client),
        "criminal-ip-ip-report": lambda: get_ip_report(client, demisto.args()),
        "criminal-ip-check-malicious-ip": lambda: check_malicious_ip(client, demisto.args()),
        "criminal-ip-check-last-scan-date": lambda: check_last_scan_date(client, demisto.args()),
        "criminal-ip-domain-quick-scan": lambda: domain_quick_scan(client, demisto.args()),
        "criminal-ip-domain-lite-scan": lambda: domain_lite_scan(client, demisto.args()),
        "criminal-ip-domain-lite-scan-status": lambda: domain_lite_scan_status(client, demisto.args()),
        "criminal-ip-domain-lite-scan-result": lambda: domain_lite_scan_result(client, demisto.args()),
        "criminal-ip-domain-full-scan": lambda: domain_full_scan(client, demisto.args()),
        "criminal-ip-domain-full-scan-status": lambda: domain_full_scan_status(client, demisto.args()),
        "criminal-ip-domain-full-scan-result": lambda: domain_full_scan_result(client, demisto.args()),
        "criminal-ip-domain-full-scan-make-email-body": lambda: make_email_body(client, demisto.args()),
        "criminal-ip-micro-asm": lambda: micro_asm(client, demisto.args()),
    }

    try:
        if command in commands:
            return_results(commands[command]())
        else:
            return_error(f"Command '{command}' is not implemented.")
    except Exception as e:
        return_error(f"Error: {str(e)}")


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