ParseCSV
This script will parse a CSV file and place the unique IPs, Domains and Hashes into the context.
- Type
- python
- Pack
- CommonScripts
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 |