ParseCSV

This script will parse a CSV file and place the unique IPs, Domains and Hashes into the context.

python · Common Scripts

Source

import csv

from CommonServerPython import *

codec_type = demisto.args().get("codec", "utf-8")


def remove_non_printable_chars(s):
    """
    removes
    'ZERO WIDTH SPACE' (U+200B)
    'ZERO WIDTH NO-BREAK SPACE' (U+FEFF)
    """
    return s.replace("\ufeff", "").replace("\u200f", "")


def unicode_dict_reader(csv_data, **kwargs):
    """
    reads from csv file each row and converts to array of dictionaries.
    in case there are extra fields in a row and they have no column, then we will create NO_NAME_COLUMN_{NUMBER}

    CSV Example:
    aaa,bbb
    1,2
    3,4,5

    ===>

    [
        {
            "aaa": 1,
            "bbb": 2,
            "NO_NAME_COLUMN_3": ""
        },
        {
            "aaa": 3,
            "bbb": 4,
            "NO_NAME_COLUMN_3": 5       <-- extra field/column
        }
    ]
    """
    csv_reader = csv.DictReader((line.replace("\0", "") for line in csv_data), **kwargs)
    arr = []
    no_name_columns_counter = 0
    for row in csv_reader:
        row_dict = {}

        for key, value in row.items():
            if key is None:
                # if the key is None it means there are fields in the row which has no column name
                # so we create NO_NAME_COLUMN_{} column
                if not isinstance(value, list):
                    value = [value]  # type: ignore

                counter = 0
                for val in value:
                    col_name = f"NO_NAME_COLUMN_{counter}"
                    row_dict[col_name] = val
                    counter += 1

                if no_name_columns_counter < counter:
                    no_name_columns_counter = counter

            elif value is not None:
                col_name = remove_non_printable_chars(key)
                row_dict[col_name] = value
            else:
                col_name = remove_non_printable_chars(key)
                row_dict[col_name] = None

        arr.append(row_dict)

    if no_name_columns_counter > 0:
        """
        adding NO_NAME_COLUMN_{} to the first dict in the array
        so that later in tableToMarkdown it will print all the columns
        """
        first_row = arr[0]
        for counter in range(no_name_columns_counter):
            first_row[f"NO_NAME_COLUMN_{counter}"] = ""

    return arr


def get_entry_by_file_name(file_name):
    entries = demisto.executeCommand("getEntries", {})
    for entry in reversed(entries):
        fn = demisto.get(entry, "File")

        if not isinstance(fn, str):
            continue

        if file_name.lower() == fn.lower():
            return entry
    raise ValueError(f'Was unable to find "{file_name}" in the war room. Please ensure the file was uploaded.')


csv_entry = None
ip_count = 0
domain_count = 0
hash_count = 0


def is_one_dimension_list(all_csv):
    """Checks if given list is one dimensional

    Args:
        all_csv (list): list of csv entries

    Returns:
        bool: True if all strings (one dimension list) or False if not
    """
    return all(isinstance(entry, STRING_TYPES) for entry in all_csv) or not all_csv


def main():
    ip_list = []
    domain_list = []
    hash_list = []
    d_args = demisto.args()

    entry_id = d_args.get("entryID", None)
    file_name = d_args.get("file", None)  # file arg deprecated
    parse_ip = int(d_args["ips"]) if "ips" in d_args else -1
    parse_domain = int(d_args["domains"]) if "domains" in d_args else -1
    parse_hash = int(d_args["hashes"]) if "hashes" in d_args else -1
    parse_all = d_args["parseAll"] == "yes"

    if parse_ip == -1 and parse_domain == -1 and parse_hash == -1 and not parse_all:
        return_error("Select a field to extract or set parseAll=yes to parse the whole CSV file")

    if file_name is None and entry_id is None:
        return_error("Please provide entryID.")

    if entry_id is None:
        # search entry by file name
        try:
            entry = get_entry_by_file_name(file_name)
            entry_id = entry["ID"]
        except ValueError as e:
            return_error(str(e))

    res = demisto.getFilePath(entry_id)
    if not res:
        return_error(f"Entry {entry_id} not found")

    file_path = res["path"]
    file_name = res["name"]
    if not file_name.lower().endswith(".csv"):
        return_error(
            f'"{file_name}" is not in csv format. Please ensure the file is in correct format and has a ".csv" extension'
        )

    if parse_all:
        all_csv = []
        with open(file_path) as f:
            records = unicode_dict_reader(f)
            # `records` is a list contains CSV rows (without headers)
            # so if it doesn't exists - it can be empty or one-lined CSV
            if records:
                for row in records:
                    all_csv.append(row)
            else:  # Can be one-line csv
                f.seek(0)
                line = f.read()
                all_csv = line.split(",")

        output = {"ParseCSV.ParsedCSV": all_csv}
        if is_one_dimension_list(all_csv):
            human_readable = tableToMarkdown(file_name, all_csv, headers=["CSV list"])
        else:
            human_readable = tableToMarkdown(file_name, all_csv)
        demisto.results(
            {
                "Type": entryTypes["note"],
                "ContentsFormat": formats["json"],
                "ReadableContentsFormat": formats["markdown"],
                "Contents": all_csv,
                "EntryContext": output,
                "HumanReadable": human_readable,
            }
        )

    elif not (parse_ip == -1 and parse_domain == -1 and parse_hash == -1):
        # if need to parse ips/domains/hashes, keep the script running
        if sum(1 for line in open(file_path)) <= 1:  # checks if there are less than one line
            return_error("No data to parse. CSV file might be empty or one-lined. try the `ParseAll=yes` argument.")

        with open(file_path) as f:
            has_header = csv.Sniffer().has_header(f.read(1024))
            f.seek(0)
            csv_data = csv.reader(f)

            if has_header:
                next(csv_data)

            md = (
                "### Parsed Data Table\n"
                + ("IPs |" if "ips" in d_args else "")
                + ("Domains |" if "domains" in d_args else "")
                + ("Hashes |" if "hashes" in d_args else "")
                + "\n"
            )
            md += (
                ("- |" if "ips" in d_args else "")
                + ("- |" if "domains" in d_args else "")
                + ("- |" if "hashes" in d_args else "")
                + "\n"
            )
            content = ""

            for row in csv_data:
                content += ",".join(row) + "\n"
                if parse_ip != -1:
                    is_ip = re.search(r"([0-9]{1,3}\.){3}[0-9]{1,3}", row[parse_ip])
                    is_valid = is_ip_valid(row[parse_ip])
                    if is_ip and is_valid:
                        ip_list.append(row[parse_ip])
                        continue

                if parse_hash != -1:
                    is_hash = re.search(r"[0-9A-Fa-f]{32,128}", row[parse_hash])
                    if is_hash:
                        hash_list.append(row[parse_hash])
                        continue

                if parse_domain != -1:
                    has_dot = "." in row[parse_domain]
                    no_spaces = " " not in row[parse_domain]
                    if has_dot and no_spaces:
                        domain_list.append(row[parse_domain])

            for c in range(max(len(ip_list), len(domain_list), len(hash_list))):
                md += ip_list[c] + "|" if len(ip_list) >= c + 1 else " |"
                md += domain_list[c] + "|" if len(domain_list) >= c + 1 else " |"
                md += hash_list[c] + "|" if len(hash_list) >= c + 1 else " |"
                md += "\n"

        context = {}  # type: dict
        if ip_list:
            old_ip_list = list(demisto.get(demisto.context(), "ips")) if demisto.get(demisto.context(), "ips") else []
            ip_list = list(set(ip_list) - set(old_ip_list))
            if len(ip_list) > 0:
                context["IP"] = []
                for ip in ip_list:
                    context["IP"].append({"Address": ip})

        if domain_list:
            old_domain_list = list(demisto.get(demisto.context(), "domains")) if demisto.get(demisto.context(), "domains") else []
            domain_list = list(set(domain_list) - set(old_domain_list))
            if len(domain_list) > 0:
                context["Domain"] = []
                for domain in domain_list:
                    context["Domain"].append({"Name": domain})

        if hash_list:
            old_hash_list = list(demisto.get(demisto.context(), "hashes")) if demisto.get(demisto.context(), "hashes") else []
            hash_list = list(set(hash_list) - set(old_hash_list))
            if len(hash_list) > 0:
                context["File"] = []
                for hash_string in hash_list:
                    if len(hash_string) == 32:
                        context["File"].append({"MD5": hash_string})
                    if len(hash_string) == 64:
                        context["File"].append({"SHA256": hash_string})
                    if len(hash_string) == 40:
                        context["File"].append({"SHA1": hash_string})

        demisto.results(
            {
                "Type": entryTypes["note"],
                "ContentsFormat": formats["text"],
                "Contents": content,
                "HumanReadable": md,
                "EntryContext": context,
            }
        )


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

README

This script will parse a CSV file and place the unique IPs, Domains and Hashes into the context.

Script Data


Name Description
Script Type python2
Tags file, csv, Utility

Used In


This script is used in the following playbooks and scripts.

  • Block IOCs from CSV - External Dynamic List

Inputs


Argument Name Description
entryID The war room entryID of the file.
file The name of the file. The file must be uploaded to the War Room.
ips The column number that contains IP Addresses. (First column is column 0)
domains The column number that contains domains. (First column is column 0)
hashes The column number that contains file hashes. (First column is column 0)
parseAll Parses and converts all of the rows in the CSV into JSON and puts them into the context.
codec The codec type used to parse the file. (some character sets are not UTF-8 supported)

Outputs


Path Description Type
IP.Address IP address found in the parsed file. Unknown
Domain.Name Domain found in the parsed file. Unknown
File.MD5 MD5 found in the parsed file. Unknown
File.SHA1 SHA1 found in the parsed file. Unknown
File.SHA256 SHA256 found in the parsed file. Unknown
ParseCSV.ParsedCSV Parsed csv in the form of JSON array. Unknown