commit 1180330b08f66ac61a44b0f1d12452fa14a1f9c1 Author: geoffrey Date: Sat Jun 22 14:32:21 2024 +0200 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..86e22b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.**.swp +__pycache__/** diff --git a/README.md b/README.md new file mode 100644 index 0000000..919d7f6 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Introduction +This tool can help SOC analyst to identify any threat + +# Implementation +First, you should create a virtualenv: + +``` +$ virtualenv ~/venv/baoSOC +$ source ~/venv/baoSOC/bin/activate +``` + +And install all packages the tool need: +``` +$ pip install -r requirements.txt +``` diff --git a/config b/config new file mode 100644 index 0000000..848508c --- /dev/null +++ b/config @@ -0,0 +1,2 @@ +api_key_vt: f4c451920a7e41ec344e16e6d36a1b7951bf23a8d224b796cb08301e65bf3114 +api_key_emailrep: foo diff --git a/config.py b/config.py new file mode 100644 index 0000000..77679ff --- /dev/null +++ b/config.py @@ -0,0 +1,19 @@ +#!/usr/bin/venv python +# -*- coding: utf-8 -*- + +VT_ATTRIBUTES_MAPPING = { + 'asn': 'str', + 'as_owner': 'int', + 'continent': 'str', + 'country': 'str', + 'last_analysis_date': 'date', + 'regional_internet_registry': 'str', + 'network': 'str', + 'ip': 'str' +} + +DNS_QUERIES_TYPE = { + 'A': 'address', + 'MX': ['exchange', 'preference'], + 'TXT': 'strings', +} diff --git a/dnsinformations.py b/dnsinformations.py new file mode 100644 index 0000000..3f352e3 --- /dev/null +++ b/dnsinformations.py @@ -0,0 +1,63 @@ +#!/usr/bin/venv python +# -*- coding: utf-8 -*- + +import whois +import dns.resolver +import dns.name +from config import DNS_QUERIES_TYPE + + +class DNSInformations: + def __init__(self, api_key, fqdn): + self._fqdn = fqdn + + def checkDomainExist(self): + try: + res_query = dns.resolver.resolve(self._fqdn, 'NS') + except dns.resolver.NoAnswer: + return False + except dns.resolver.NXDOMAIN: + return False + return True + + def whois(self): + report = dict() + w = whois.whois(self._fqdn) + report['domain_name'] = w.domain_name + report['expiration_date'] = w.expiration_date + report['creation_date'] = w.creation_date + report['updated_date'] = w.updated_date + report['ns'] = w.name_servers + report['admin_name'] = w.admin_name + report['registrar'] = w.registrar + return report + + def resolver(self): + report = dict() + + for t in DNS_QUERIES_TYPE.keys(): + report[t] = self._resolving(self._fqdn, t, DNS_QUERIES_TYPE[t]) + return report + + def _resolving(self, fqdn, t, attr): + report = list() + try: + res_query = dns.resolver.resolve(fqdn, t) + for rdata in res_query: + if isinstance(attr, list): + data = dict() + for a in attr: + data[a] = getattr(rdata, a) + report.append(data) + else: + report.append({ + attr: getattr(rdata, attr) + }) + except dns.resolver.NoAnswer: + pass + except dns.resolver.NXDOMAIN: + report.append({'error': 'Domain not exist'}) + except dns.resolver.NoNameservers: + report.append({'error': 'Domain not exist'}) + return report + diff --git a/emailchecker.py b/emailchecker.py new file mode 100644 index 0000000..b420616 --- /dev/null +++ b/emailchecker.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +class EmailChecker: + def __init__(self, key): + self.headers = { + 'Key': key, + } diff --git a/hashing.py b/hashing.py new file mode 100644 index 0000000..efa5063 --- /dev/null +++ b/hashing.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 + +import hashlib + + +class Hash: + def __init__(self): + pass + + def hashMd5(self, f): + md5 = hashlib.md5() + with open(f, "rb") as data: + md5.update(data.read()) + return md5.hexdigest() + + def hashSha1(self, f): + sha1 = hashlib.sha1() + with open(f, "rb") as data: + sha1.update(data.read()) + return sha1.hexdigest() + + def hashSha256(self, f): + sha256 = hashlib.sha256() + with open(f, "rb") as data: + sha256.update(data.read()) + return sha256.hexdigest() + + def hashSha384(self, f): + sha384 = hashlib.sha384() + with open(f, "rb") as data: + sha384.update(data.read()) + return sha384.hexdigest() + + def hashSha512(self, f): + sha512 = hashlib.sha512() + with open(f, "rb") as data: + sha512.update(data.read()) + return sha512.hexdigest() diff --git a/main.py b/main.py new file mode 100644 index 0000000..dd2ad74 --- /dev/null +++ b/main.py @@ -0,0 +1,259 @@ +#!/usr/bin/venv python +# -*- coding: utf-8 -*- + +from argparse import ArgumentParser +from config import VT_ATTRIBUTES_MAPPING +from vt import VT +from dnsinformations import DNSInformations as DNS +import ipaddress +from datetime import datetime +from hashing import Hash +from os.path import exists + + +def checkArguments(): + parser = ArgumentParser(description="baoSOC") + parser.add_argument('-c', '--config', help='Config file') + parser.add_argument('--dns', help='Get domain name information', action="store_true") + # For dns command + parser.add_argument('--domain', help='Get domain name information') + parser.add_argument('--host', help='Get domain name information') + parser.add_argument('--ip', help='Get IP information') + # For hash command + parser.add_argument('--hashfile', help='Hash file', action='store_true') + parser.add_argument('--scanvt', help='If specified, scan the hash with VirusTotal', action='store_true') + parser.add_argument('--md5', help='Hash file') + parser.add_argument('--sha1', help='Hash file') + parser.add_argument('--sha256', help='Hash file') + parser.add_argument('--sha384', help='Hash file') + parser.add_argument('--sha512', help='Hash file') + parser.add_argument('--hash', help='Get information about the hash') + + return parser.parse_args() + +def usage(): + print("------------------------------") + print("| baoSOC |") + print("------------------------------\n") + print("A tool for SOC analyst\n") + print("Usage: main.py [COMMAND]") + print("-c PATH, --config PATH\t\tConfig file - mandatory") + print("--hashfile\t\t\tHash the file and check in VirusTotal") + print("--hash HASH\t\tAnalyse the hash from VirusTotal") + print("--dns \t\t\t\tGet information regarding the domain with whois and VirusTotal") + print("--email\t\t\t\tGet informations about an email and check if has been compromised") + + print("\n--dns command:") + print("\t --domain FQDN\t\tScan and get domain information") + print("\t --host HOST\t\tScan and get host information") + print("\t --ip IP\t\tScan and get IP information") + + print("\n--hashfile command:") + print("\t --md5 FILE\t\tGet the MD5 hash of the file") + print("\t --sha1 FILE\t\tGet the SHA256 of the file") + print("\t --sha256 FILE\t\tGet the SHA256 of the file") + print("\t --sha384 FILE\t\tGet the SHA384 of the file") + print("\t --sha512 FILE\t\tGet the SHA512 of the file") + +def mainMenu(): + print("\n baoSOC ") + print(" What would you like to do? ") + print("\n OPTION 1: Sanitise URL For emails ") + print(" OPTION 2: Decoders (PP, URL, SafeLinks) ") + print(" OPTION 3: Reputation Checker") + print(" OPTION 4: DNS Tools") + print(" OPTION 5: Hashing Function") + print(" OPTION 6: Phishing Analysis") + print(" OPTION 7: URL scan") + print(" OPTION 9: Extras") + print(" OPTION 0: Exit Tool") + +def readConfigFile(config): + """ + This function read the config file + """ + data = {} + try: + with open(config, 'r') as f: + lines = f.readlines() + + # Split each line into te dictionary + for line in lines: + l = line.split(":") + lineParsed = l[1].replace(" ", "") + lineParsed = lineParsed.replace("\n", "") + data[l[0]] = lineParsed + + except FileNotFoundError: + return None + return data + +def main(): + args = checkArguments() + + if not args.config: + usage() + exit(1); + + # Read the config file + config = readConfigFile(args.config) + if config is None: + print("Failed to read the config file") + exit(0) + + report = dict() + + # Analyse DNS + if args.dns: + if args.domain: + _parsingDomain(config, args.domain, report) + if args.host: + pass + if args.ip: + _parsingIP(config, args.ip, report) + # Analyse hash file + if args.hashfile: + h = Hash() + dispatcher = { + "MD5": h.hashMd5, + "SHA1": h.hashSha1, + "SHA256": h.hashSha256, + "SHA384": h.hashSha384, + "SHA512": h.hashSha512, + } + if args.md5: + hashType = "MD5" + filename = args.md5 + if args.sha1: + hashType = "SHA1" + filename = args.sha1 + if args.sha256: + hashType = "SHA256" + filename = args.sha256 + if args.sha384: + hashType = "SHA384" + filename = args.sha384 + if args.sha512: + hashType = "SHA512" + filename = args.sha512 + + if not exists(filename): + print(f"File {filename} do not exist") + else: + res = dispatcher[hashType](filename) + print(f"{hashType} hash: {res}") + + if args.scanvt: + _parsingHash(config, res, report) + + # Analyse the hash + if args.hash: + _parsingHash(config, args.hash, report) + +def _parsingHash(config, h, report): + report = dict() + vt = VT(config['api_key_vt']) + + vt.getRateFromHash(h, report) + print("\n----------------------") + print("| VirusTotal |") + print("----------------------") + + if 'error' in report: + print("Error, can not find any informations") + return + + # File + print("\nFile information:") + f = report['results']['file'] + print(f"\tMD5: {f['md5']}") + print(f"\tSHA1: {f['sha1']}") + print(f"\tMagic: {f['magic']}") + print(f"\tFile type: {f['filetype']}") + print(f"\tSize: {f['size']}") + print(f"\tExtension: {f['extension']}") + print(f"\tFirst seen: {f['first_seen']}") + print(f"\tLast analysis: {f['last_analysis']}") + + # Vendors + vendors = report['results']['vendors'] + print("\nVendors:") + print(f"\tTotal vendors: {vendors['total_vendors']}") + print(f"\tCategorized as malicious: {vendors['total_malicious']}\n") + for key in vendors['result']: + print(f"\t{key}: {vendors['result'][key]}") + +def _parsingIP(config, ip, report): + # Check if it's an IP address + vt = VT(config['api_key_vt']) + try: + ip_obj = ipaddress.ip_address(ip) + except ValueError: + print("The IP is not valid") + return + + report['ip'] = dict() + report['ip'] = vt.getIPVirusTotal(ip) + + if 'error' not in report['ip']: + print("----------------------------") + print("| IP Informations |") + print("----------------------------") + for vt in VT_ATTRIBUTES_MAPPING.keys(): + try: + vtAttributes = VT_ATTRIBUTES_MAPPING[vt] + if 'date' in vtAttributes: + value = datetime.fromtimestamp(int(report['ip'][vt])) + else: + value = report['ip'][vt] + print(f"{vt}: {value}") + except KeyError: + print(f"Cannot find the key {vt}") + +def _parsingDomain(config, fqdn, report): + vt = VT(config['api_key_vt']) + dns = DNS(config['api_key_vt'], fqdn) + + # Check if domain exist + if not dns.checkDomainExist(): + print(f"The domain {fqdn} do not exist") + return + + print("----------------------") + print("| resolving |") + print("----------------------") + report['resolving'] = dns.resolver() + for key in report['resolving'].keys(): + print(f"{key}: ") + for entry in report['resolving'][key]: + for subkey in entry.keys(): + value = entry[subkey] + if isinstance(value, bytes): + value = value.decode() + print(f"\t{subkey}: {value}") + + print("\n----------------------") + print("| whois |") + print("----------------------") + report['whois'] = dns.whois() + report_whois = report['whois'] + for key in report_whois.keys(): + if isinstance(report_whois[key], list): + print(f"{key}:") + for value in report_whois[key]: + print(f"\t{value}") + else: + print(f"{key}: {report_whois[key]}") + + print("\n----------------------") + print("| VirusTotal |") + print("----------------------") + report['vt'] = dict() + vt.getDomainReport(fqdn, report['vt']) + report_vt = report['vt'] + for key in report_vt: + print(f"{key}: {report_vt[key]}") + + +if __name__ == "__main__": + main() diff --git a/reports.py b/reports.py new file mode 100644 index 0000000..3188c7b --- /dev/null +++ b/reports.py @@ -0,0 +1,157 @@ +#!/usr/bin/venv python +# -*- coding: utf-8 -*- + +from datetime import datetime +from os import mkdir +from config import VT_ATTRIBUTES_MAPPING +import jinja2 + + +def generateHtmlReport(data): + env = jinja2.Environment( + loader=jinja2.FileSystemLoader("reports/templates"), + autoescape=jinja2.select_autoescape() + ) + + _queriesReport(data['queries'], env) + _graphicsReport(data['graphics'], env) + _vtReport(data['vt'], env) + _dnsTunnelingReports(data['dnstunneling'], env) + +def _indexReport(): + pass + +def _queriesReport(queries, env): + """ + This function generate the report for queries + """ + today = getTodayDate() + dataJinja2 = dict() + dataJinja2['title'] = 'Queries' + dataJinja2['year'] = '2023' + dataJinja2['queries'] = queries + + tmpl = env.get_template('queries.html.j2') + + render = tmpl.render(data=dataJinja2) + + with open(f"reports/{today}/reports_queries.html", "w") as f: + f.write(render) + +def _graphicsReport(graphics, env): + today = getTodayDate() + + dataJinja2 = dict() + dataJinja2['title'] = 'Graphics' + dataJinja2['year'] = '2023' + dataJinja2['graphics'] = graphics + + tmpl = env.get_template('graphics.html.j2') + + render = tmpl.render(data=dataJinja2) + + with open(f"reports/{today}/reports_graphics.html", "w") as f: + f.write(render) + +def _vtReport(vt, env): + today = getTodayDate() + # For testing + #vt = list() + #vt.append({ + # 'ip': '1.2.3.4', + # 'asn': 3215, + # 'as_owner': 'Orange', + # 'continent': 'EU', + # 'country': 'FR', + # 'last_analysis_date': 1686839532, + # 'regional_internet_registry': 'RIPE NCC', + # 'network': '1.2.3.0/24' + #}) + #vt.append({ + # 'ip': '2.2.2.1', + # 'asn': 3215, + # 'as_owner': 'Orange', + # 'continent': 'EU', + # 'country': 'FR', + # 'last_analysis_date': 1686839532, + # 'regional_internet_registry': 'RIPE NCC', + # 'network': '2.2.2.0/24' + #}) + #vt.append({ + # 'ip': '3.3.3.1', + # 'asn': 3215, + # 'as_owner': 'Orange', + # 'continent': 'EU', + # 'country': 'FR', + # 'last_analysis_date': 1686839532, + # 'regional_internet_registry': 'RIPE NCC', + # 'network': '3.3.3.0/24' + #}) + + dataJinja2 = dict() + dataJinja2['title'] = 'VirusTotal' + dataJinja2['year'] = '2023' + dataJinja2['vt'] = list() + + tmpl = env.get_template('vt.html.j2') + + body = str() + + for entry in vt: + vtEntry = dict() + if 'error' not in entry: + for vt in VT_ATTRIBUTES_MAPPING.keys(): + try: + vtAttributes = VT_ATTRIBUTES_MAPPING[vt] + if 'date' in vtAttributes: + value = datetime.fromtimestamp(int(entry[vt])) + else: + value = entry[vt] + vtEntry[vt] = value + except KeyError: + pass + dataJinja2['vt'].append({ + 'ip': entry['ip'], + 'data': vtEntry + }) + + render = tmpl.render(data=dataJinja2) + + with open(f"reports/{today}/reports_vt.html", "w") as f: + f.write(render) + +def _dnsTunnelingReports(dnstunneling, env): + today = getTodayDate() + + dataJinja2 = dict() + dataJinja2['title'] = 'DNS Tunneling' + dataJinja2['year'] = '2023' + dataJinja2['dnstunneling'] = dnstunneling + + tmpl = env.get_template('dnsTunneling.html.j2') + + render = tmpl.render(data=dataJinja2) + + with open(f"reports/{today}/reports_dns_tunneling.html", "w") as f: + f.write(render) + +def createReportsDirectory(): + """ + This function will create the reports directory + Return the report name or None if failed + """ + today = getTodayDate() + name = f"reports/{today}" + try: + mkdir(name) + except FileExistsError: + print("Reports directory already created") + return name + return name + +def getTodayDate(): + """ + This function genrate the today datetime at this format: + year_month_day + """ + return datetime.now().isoformat()[0:10].replace("-", "_") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d5168c2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,10 @@ +pip +scapy +ArgumentParser +matplotlib +numpy +vt-py +requests +jinja2 +python-whois +git+https://github.com/rthalley/dnspython.git diff --git a/vt.py b/vt.py new file mode 100644 index 0000000..702a72f --- /dev/null +++ b/vt.py @@ -0,0 +1,114 @@ +#!/ur/bin/env python3 + +import requests +from config import VT_ATTRIBUTES_MAPPING +from datetime import datetime + + +class VT: + def __init__(self, api_key): + self._url = "https://www.virustotal.com/api/v3" + self._headers = { + 'x-apikey': api_key, + } + + def getIPVirusTotal(self, ip): + res = requests.get( + f"{self._url}/ip_addresses/{ip}", + headers=self._headers + ).json() + + data = dict() + data['ip'] = ip + + if 'error' in res: + report.append({ + 'error': res['error']['message'], + 'ip': ip + }) + return + + vt = res['data']['attributes'] + for entry in VT_ATTRIBUTES_MAPPING.keys(): + if entry in vt: + try: + data[entry] = vt[entry] + except KeyError: + data[entry] = 'Unknown' + return data + + def getDomainReport(self, fqdn, report): + res = requests.get( + f"{self._url}/domains/{fqdn}", + headers=self._headers + ) + + js = res.json() + report['reputation'] = js['data']['attributes']['reputation'] + report['last_update'] = \ + datetime.fromtimestamp(js['data']['attributes']['last_update_date']) + + # Get number of security vendors + report['total_vendors'] = 0 + report['clean'] = 0 + report['unrated'] = 0 + report['malicious'] = 0 + vendors = js['data']['attributes']['last_analysis_results'] + + for entry in vendors: + report['total_vendors'] += 1 + if vendors[entry]['result'] == 'clean': + report['clean'] += 1 + elif vendors[entry]['result'] == 'unrated': + report['unrated'] += 1 + elif vendors[entry]['result'] == 'malicious': + report['malicious'] += 1 + + def getRateFromHash(self, h, report): + headers = self._headers + + res = requests.get( + f"{self._url}/files/{h}", + headers=self._headers + ).json() + + if 'error' in res: + report["error"] = "Can not find the result" + return + + attributes = res['data']['attributes'] + report['results'] = dict() + report['results']['file'] = dict() + report['results']['file']['magic'] = attributes['magic'] + report['results']['file']['sha1'] = attributes['sha1'] + report['results']['file']['md5'] = attributes['md5'] + report['results']['file']['filetype'] = attributes['detectiteasy']['filetype'] + report['results']['file']['size'] = attributes['size'] + report['results']['file']['extension'] = attributes['type_extension'] + try: + report['results']['file']['first_seen'] = \ + datetime.fromtimestamp(attributes['first_seen_itw_date']) + except KeyError: + report['results']['file']['first_seen'] = "Unknown" + + report['results']['file']['last_analysis'] = \ + datetime.fromtimestamp(attributes['last_analysis_date']) + + # Identify vendors + report['results']['vendors'] = dict() + report['results']['vendors']['total_vendors'] = 0 + report['results']['vendors']['total_malicious'] = 0 + report['results']['vendors']['result'] = dict() + report['results']['vendors']['result']['undetected'] = 0 + results = res['data']['attributes']['last_analysis_results'] + for entry in results: + report['results']['vendors']['total_vendors'] += 1 + if results[entry]['category'] == 'undetected': + report['results']['vendors']['result']['undetected'] += 1 + else: + result = results[entry]['result'] + if result not in report['results']['vendors']['result']: + report['results']['vendors']['result'][result] = 0 + report['results']['vendors']['result'][result] += 1 + report['results']['vendors']['total_malicious'] += 1 +