PhishTank V2

PhishTank is a free community site where anyone can submit, verify, track, and share phishing data.

Data Enrichment & Threat Intelligence · PhishTank

Details

IDPhishTank V2
ProviderOpen Source
CategoryData Enrichment & Threat Intelligence
From Version5.0.0
Docker Imagedemisto/python3:3.12.13.10325753
Supported ModulesAgentix XSIAM

README

PhishTank is a free community site where anyone can submit, verify, track and share phishing data.
This integration was integrated and tested with version 1.0.1 of PhishTank.

Configure PhishTankV2 in Cortex

Parameter Description Required
use_https Use HTTPS connection False
Source Reliability Reliability of the source providing the intelligence data. B - Usually reliable
proxy Use system proxy settings False
insecure Trust any certificate (not secure) False
fetchIntervalHours Database refresh interval (hours) False

Best Practice

When using the PhishTank V2 integration, we recommend that you use an engine to run the integration instance,
and to use different engines for different tenants.
You should open a platform feature request (FR) to request separate egress IPs for the different tenants.

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.

url


Checks the reputation of the supplied URLs.

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

Base Command

url

Input

Argument Name Description Required
url A comma-separated list of URLs to check the reputation of. Required

Context Output

Path Type Description
URL.Data String A list of URLs with a bad reputation.
URL.Malicious.Vendor String For malicious URLs, the vendor that tagged the URL as malicious.
URL.Malicious.Description String For malicious URLs, the reason the vendor tagged the URL as malicious.
DBotScore.Indicator String The indicator that was tested.
DBotScore.Type String The indicator type.
DBotScore.Vendor String The vendor used to calculate the score.
DBotScore.Score Number The actual score.
DBotScore.Reliability String Reliability of the source providing the intelligence data.

Command Example

!url url=hxxp://login.rakuten.co.jp.reise

Human Readable Output

PhishTankV2 Database - URL Query

Found matches for URL hxxp://login.rakuten.co.jp.reise

online phish_id submission_time target verification_time verified
yes 6784982 2020-09-27T19:04:35+00:00 Other 2020-09-27T19:10:20+00:00 yes

Additional details at http://www.phishtank.com/phish_detail.php?phish_id=6784982

phishtank-reload


Reload PhishTank database

Base Command

phishtank-reload

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example

!phishtank-reload

Human Readable Output

PhishTankV2 Database reloaded

Total 13181 URLs loaded

phishtank-status


Show PhishTank database status

Base Command

phishtank-status

Input

There are no input arguments for this command.

Context Output

There is no context output for this command.

Command Example

!phishtank-status

Human Readable Output

PhishTankV2 Database Status

Total 13181 URLs loaded

Last Load time Sun Oct 04 2020 09:43:01 (UTC)

Configuration parameters

  • username — Username
  • use_https — Use HTTPS connection
  • integrationReliability — Source Reliability (required)
  • fetchIntervalHours — Database refresh interval (number of hours to wait between each loading)
  • proxy — Use system proxy settings
  • insecure — Trust any certificate (not secure)

Commands (3)

  • phishtank-reload

    Reloads the PhishTank downloadable database. PhishTank database is an up to date phishing detection file (updated hourly) .

  • phishtank-status

    Shows the status (timestamp) of the last time that PhishTank database was loaded.

  • url

    Checks the reputation of the supplied URLs.

import urllib3
from CommonServerPython import *

RESPONSE_LINE_LENGTH = 8

urllib3.disable_warnings()

""" CONSTANTS """

BASE_URL = "http://data.phishtank.com"
HTTPS_BASE_URL = "https://data.phishtank.com"
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
RELOAD_DATA_URL_SUFFIX = "data/online-valid.csv"

""" CLIENT CLASS """


def handle_error(res: requests.models.Response):
    if res.status_code in (404, 509, 429):
        err_msg = f"PhishTankV2 - Error in API call {res.status_code} - {res.reason}"
        if res.status_code == 429:
            err_msg += f', Please try again in {res.headers.get("Retry-after")} seconds'
        return_error(err_msg)


class Client(BaseClient):
    """
    Client to use in the PhisTankV2 integration. Overrides BaseClient.

        Args:
           proxy (bool): Whether the client should use proxies.
           verify (bool): Whether to check for SSL certificate validity.
           fetch_interval_hours (str) : Database refresh interval (hours)
           use_https (bool): Whether to use HTTPS URL or HTTP URL.
    """

    def __init__(
        self, proxy: bool, verify: bool, fetch_interval_hours: str, use_https: str, reliability: str, username: str = ""
    ):
        super().__init__(proxy=proxy, verify=verify, base_url=HTTPS_BASE_URL if use_https else BASE_URL)
        self.fetch_interval_hours = fetch_interval_hours
        self.username = username

        if DBotScoreReliability.is_valid_type(reliability):
            self.reliability = DBotScoreReliability.get_dbot_score_reliability_from_str(reliability)
        else:
            return_error("PhishTankV2 error: Please provide a valid value for the Source Reliability parameter.")

    def get_http_request(self, url_suffix: str):
        headers = {}
        if self.username:
            headers = {"User-Agent": f"phishtank/{self.username}"}
        result = self._http_request(
            method="GET", url_suffix=url_suffix, resp_type="text", headers=headers, error_handler=handle_error
        )
        return result


""" COMMAND FUNCTIONS """


def test_module(client: Client) -> str:
    """Tests API connectivity and authentication'"""
    data = reload(client)
    response_was_empty = len(data.keys()) == 0
    if response_was_empty:
        return_error("Error - could not fetch PhishTankV2 database, API returned an empty response")
    return "ok"


def was_phishtank_data_ever_reloaded(context: dict):
    """
    Checking if PhishTank data was ever reloaded by checking IntegrationContext. (IntegrationContext set during
    the reload command).

    Args:
        context (dict) : IntegrationContext that is empty / contains PhishTank data.

    Returns: True if context contains PhishTank data (from a previous reload). False otherwise.

    """
    was_phishtank_data_reloaded = context != {}
    return bool(was_phishtank_data_reloaded)


def is_phishtank_data_outdated(client: Client, context: dict):
    """
    Checks if last last reload was in the last fetch_interval_hours or not.

    Args:
        client: Client to use in the PhisTankV2 integration.
        context (dict):  IntegrationContext contains PhishTank data.

    Returns:
        True if last reload was much than fetch_interval_hours ago.
        False otherwise.

    """
    current_time = datetime.now()
    fetch_interval_seconds = timedelta(hours=float(client.fetch_interval_hours))
    return context["timestamp"] < date_to_timestamp(current_time - fetch_interval_seconds)


def is_reload_needed(client: Client, context: dict) -> bool:
    """

    Args:
        client: Client to use in the PhisTankV2 integration.
        context: IntegrationContext .
                - "list" contains data from http response
                - "timestamp" datatime of the last response

    Returns: True if DB can be loaded now. i.e DB was not loaded in the last fetch_interval hours.
            False otherwise.
    """
    return not was_phishtank_data_ever_reloaded(context) or is_phishtank_data_outdated(client, context)


def get_url_data(client: Client, url: str):
    url = remove_last_slash(url)
    integration_context = get_integration_context()
    current_data_url = None
    if is_reload_needed(client, integration_context):
        data = reload(client)
        current_date = date_to_timestamp(datetime.now(), DATE_FORMAT)
        context = {"list": data, "timestamp": current_date}
        set_integration_context(context)
        data_contains_url = url in data
        if data_contains_url:
            current_data_url = data[url]
    else:
        url_was_reloaded = url in integration_context["list"]
        if url_was_reloaded:
            current_data_url = integration_context["list"][url]
    return current_data_url, url


def url_data_to_dbot_score(url_data: dict, url: str, reliability: DBotScoreReliability):
    if url_data["verified"] == "yes":
        dbot_score = 3
    else:
        dbot_score = 2
    return Common.DBotScore(url, DBotScoreType.URL, "PhishTankV2", dbot_score, "Match found in PhishTankV2 database", reliability)


def create_verified_markdown(url_data: dict, url: str):
    markdown = f"#### Found matches for URL {url} \n"
    markdown += tableToMarkdown("", url_data)
    phish_tank_url = f'http://www.phishtank.com/phish_detail.php?phish_id={url_data["phish_id"]}'
    phish_tank_url = create_clickable_url(phish_tank_url)
    markdown += f"Additional details at {phish_tank_url} \n"
    return markdown


def url_command(client: Client, url_list: list) -> List[CommandResults]:
    command_results: List[CommandResults] = []
    for url in url_list:
        markdown = "### PhishTankV2 Database - URL Query \n"
        url_data, url = get_url_data(client, url)
        url_data_is_valid = url_data and "verified" in url_data
        if url_data_is_valid:
            dbot = url_data_to_dbot_score(url_data, url, client.reliability)
            markdown += create_verified_markdown(url_data, url)
        else:
            markdown += f"#### No matches for URL {url} \n"
            dbot = Common.DBotScore(url, DBotScoreType.URL, "PhishTankV2", 0, "", client.reliability)
        command_results.append(
            CommandResults(
                indicator=Common.URL(url, dbot),
                readable_output=markdown,
            )
        )

    return command_results


def phishtank_reload_command(client: Client):
    """
    Requests a csv file from PhishTank.
    Sets the response in IntegrationContext.
    Args:
        client:  Client to use in the PhisTankV2 integration.

    Returns: CommandResults:
            - readable_output (str): number of urls that reloaded during that reload.

    """
    parsed_response = reload(client)  # gets a parsed response
    current_date = date_to_timestamp(datetime.now(), DATE_FORMAT)
    context = {"list": parsed_response, "timestamp": current_date}
    set_integration_context(context)
    readable_output = "PhishTankV2 Database reloaded \n"
    number_of_urls_loaded = len(parsed_response.keys())
    readable_output += f"Total **{number_of_urls_loaded}** URLs loaded.\n"
    last_load = datetime.utcfromtimestamp(context["timestamp"] / 1000.0).strftime("%a %b %d %Y %H:%M:%S (UTC)")
    output_to_context = {"value": last_load}
    return CommandResults(readable_output=readable_output, outputs=output_to_context, outputs_prefix="LastReloadTime(obj)")


def phishtank_status_command():
    """
    Checks in IntegrationContext if data was reloaded so far or not.
    note : IntegrationContext updated in each reload command.

    Returns: CommandResults:
        - readable_output (str) : contains the number of urls that were reloaded in the last reload and the date
                                of the last reload.
    """
    data = get_integration_context()
    status = "PhishTankV2 Database Status\n"
    data_was_not_reloaded_yet = data == {}
    last_load = ""
    if data_was_not_reloaded_yet:
        status += "Database not loaded.\n"
    else:
        last_load = datetime.utcfromtimestamp(data["timestamp"] / 1000.0).strftime("%a %b %d %Y %H:%M:%S (UTC)")
        number_of_urls_loaded = len(data["list"].keys())
        status += f"Total **{number_of_urls_loaded}** URLs loaded.\nLast Load time **{last_load}**\n"
    output_to_context = {"value": last_load}
    return CommandResults(readable_output=status, outputs=output_to_context, outputs_prefix="LastReloadTime(obj)")


def reload(client: Client) -> dict:
    """
    This function is responsible for:
     1. request a csv file from PhishTank API (calling to client.get_http_request)
     2. parsing an API response and saving all relevant information into a dictionary

    Args:
        client:
        (Client) : client to use in the PhisTankV2 integration.

    Returns:
        dictionary of parsed http response. Each url is a key and his values are:
            "id,submission_time,verified,verification_time,online,target"
    """
    response = client.get_http_request(RELOAD_DATA_URL_SUFFIX)
    response_is_empty = not response
    if response_is_empty:
        return {}
    response = response.splitlines()
    parsed_response = {}
    columns = response[0].strip().split(",")  # get csv headers
    for index, line in list(enumerate(response))[1:]:
        line = line.split(",")
        line = parse_response_line(line, index, response)
        invalid_parsed_line = line is None
        if invalid_parsed_line:
            continue
        url = remove_last_slash(line[columns.index("url")])
        if url:
            parsed_response[url] = {
                "phish_id": line[columns.index("phish_id")].strip(),
                "submission_time": line[columns.index("submission_time")].strip(),
                "verified": line[columns.index("verified")].strip(),
                "verification_time": line[columns.index("verification_time")].strip(),
                "online": line[columns.index("online")].strip(),
                "target": line[columns.index("target")].strip(),
            }
    return parsed_response


def parse_response_line(current_line: list, index: int, response: list):
    """
    This function checks if current line is a valid line.
    note: there is a specific line in PhishTank csv response that is broken into 2 following lines. In this case,
            those 2 lines are concatenate into one complete line.
    Args:
        current_line (list): current response's line to be parsed
        index (int) : current line's index
        response (list) : list of PhishTank csv response

    Returns: line (str): the parsed line

    """
    current_line_length = len(current_line)
    # RESPONSE_LINE_LENGTH is the number of valid columns in csv
    line_is_broken = current_line_length < RESPONSE_LINE_LENGTH
    if line_is_broken:
        next_line = response[index + 1].strip().split(",")
        current_line_has_missing_columns = len(next_line) >= RESPONSE_LINE_LENGTH
        if current_line_has_missing_columns:
            # this next_line is not the second part of current_line.
            # i.e current_line is not valid  - because next_line is not the continuation of current line
            return None
        else:
            # this is the second part of broken line. i.e current_line + next_line should have been one complete line
            return current_line + next_line
    return current_line


def remove_last_slash(url: str) -> str:
    url = url.strip()
    if len(url) > 0 and url[-1] == os.sep:
        return url[:-1]
    return url


def is_number(fetch_interval_hours: str) -> bool:
    try:
        return float(fetch_interval_hours) > 0
    except ValueError:
        return False


""" MAIN FUNCTION """


def main() -> None:
    params = demisto.params()
    use_https = params.get("use_https", False)
    proxy = params.get("proxy")
    verify = not params.get("insecure")
    fetch_interval_hours = params.get("fetchIntervalHours") or "1"
    reliability = params.get("integrationReliability")
    username = params.get("username")

    if not is_number(fetch_interval_hours):
        return_error(
            "PhishTankV2 error: Please provide a numeric value (and bigger than 0) for Database refresh interval (hours)"
        )

    # initialize a client
    client = Client(proxy, verify, fetch_interval_hours, use_https, reliability, username)

    command = demisto.command()
    demisto.debug(f"PhishTankV2: command is {command}")

    try:
        if command == "test-module":
            return_results(test_module(client))

        elif command == "url":
            url = argToList(demisto.args().get("url"))
            return_results(url_command(client, url))

        elif command == "phishtank-reload":
            return_results(phishtank_reload_command(client))

        elif command == "phishtank-status":
            return_results(phishtank_status_command())

    # Log exceptions and return errors
    except Exception as e:
        return_error(f"Failed to execute {command} command.\nError:\n{e!s}")


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