|
25 | 25 | inquestlabs [options] yara widere <regex> [(--big-endian|--little-endian)]
|
26 | 26 | inquestlabs [options] lookup ip <ioc>
|
27 | 27 | inquestlabs [options] lookup domain <ioc>
|
| 28 | + inquestlabs [options] report <ioc> |
28 | 29 | inquestlabs [options] stats
|
29 | 30 | inquestlabs [options] setup <apikey>
|
30 | 31 | inquestlabs [options] trystero list-days
|
|
44 | 45 | --little-endian Toggle little endian.
|
45 | 46 | --offset=<offset> Specify an offset other than 0 for the trigger.
|
46 | 47 | --proxy=<proxy> Intermediate proxy
|
| 48 | + --timeout=<timeout> Maximum amount of time to wait for IOC report. |
47 | 49 | --verbose=<level> Verbosity level, outputs to stderr [default: 0].
|
48 | 50 | --version Show version.
|
49 | 51 | """
|
|
68 | 70 | pass
|
69 | 71 |
|
70 | 72 | # standard libraries.
|
| 73 | +import multiprocessing |
| 74 | +import ipaddress |
71 | 75 | import hashlib
|
72 | 76 | import random
|
73 | 77 | import time
|
|
76 | 80 | import os
|
77 | 81 | import re
|
78 | 82 |
|
79 |
| -__version__ = 1.0 |
| 83 | +__version__ = 1.1 |
80 | 84 |
|
81 |
| -VALID_CAT = ["ext", "hash", "ioc"] |
82 |
| -VALID_EXT = ["code", "context", "metadata", "ocr"] |
83 |
| -VALID_HASH = ["md5", "sha1", "sha256", "sha512"] |
84 |
| -VALID_IOC = ["domain", "email", "filename", "filepath", "ip", "registry", "url", "xmpid"] |
| 85 | +VALID_CAT = ["ext", "hash", "ioc"] |
| 86 | +VALID_EXT = ["code", "context", "metadata", "ocr"] |
| 87 | +VALID_HASH = ["md5", "sha1", "sha256", "sha512"] |
| 88 | +VALID_IOC = ["domain", "email", "filename", "filepath", "ip", "registry", "url", "xmpid"] |
| 89 | +VALID_DOMAIN = re.compile("[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+") |
85 | 90 |
|
86 | 91 | # verbosity levels.
|
87 | 92 | INFO = 1
|
88 | 93 | DEBUG = 2
|
89 | 94 |
|
| 95 | +######################################################################################################################## |
| 96 | +def worker_proxy (labs, endpoint, arguments, response): |
| 97 | + """ |
| 98 | + proxy function for multiprocessing wrapper used by inquestlabs_api.report() |
| 99 | + """ |
| 100 | + |
| 101 | + response[endpoint] = getattr(labs, endpoint)(*arguments) |
| 102 | + |
| 103 | + |
90 | 104 | ########################################################################################################################
|
91 | 105 | class inquestlabs_exception(Exception):
|
92 | 106 | pass
|
@@ -814,6 +828,59 @@ def iocdb_sources (self):
|
814 | 828 |
|
815 | 829 | return self.API("/iocdb/sources")
|
816 | 830 |
|
| 831 | + ######################################################################################################################## |
| 832 | + def is_ipv4 (self, s): |
| 833 | + # we prefer to use the ipaddress third-party module here, but fall back to a regex solution. |
| 834 | + try: |
| 835 | + import ipaddress |
| 836 | + except: |
| 837 | + if re.match("^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$", s): |
| 838 | + return True |
| 839 | + else: |
| 840 | + return False |
| 841 | + |
| 842 | + # python 2/3 compat |
| 843 | + try: |
| 844 | + s = unicode(s) |
| 845 | + except: |
| 846 | + pass |
| 847 | + |
| 848 | + # is instance of IPv4 address? |
| 849 | + try: |
| 850 | + return isinstance(ipaddress.ip_address(s), ipaddress.IPv4Address) |
| 851 | + except: |
| 852 | + return False |
| 853 | + |
| 854 | + |
| 855 | + ######################################################################################################################## |
| 856 | + def is_ipv6 (self, s): |
| 857 | + # best effort pull in third-party module. |
| 858 | + try: |
| 859 | + import ipaddress |
| 860 | + except: |
| 861 | + return None |
| 862 | + |
| 863 | + # python 2/3 compat |
| 864 | + try: |
| 865 | + s = unicode(s) |
| 866 | + except: |
| 867 | + pass |
| 868 | + |
| 869 | + # is instance of IPv6 address? |
| 870 | + try: |
| 871 | + return isinstance(ipaddress.ip_address(s), ipaddress.IPv6Address) |
| 872 | + except: |
| 873 | + return False |
| 874 | + |
| 875 | + |
| 876 | + #################################################################################################################### |
| 877 | + def is_domain (self, s): |
| 878 | + return VALID_DOMAIN.match(s) |
| 879 | + |
| 880 | + #################################################################################################################### |
| 881 | + def is_ip (self, s): |
| 882 | + return self.is_ipv4(s) or self.is_ipv6(s) |
| 883 | + |
817 | 884 | ####################################################################################################################
|
818 | 885 | def lookup (self, kind, ioc):
|
819 | 886 | """
|
@@ -920,6 +987,86 @@ def repdb_sources (self):
|
920 | 987 |
|
921 | 988 | return self.API("/repdb/sources")
|
922 | 989 |
|
| 990 | + #################################################################################################################### |
| 991 | + def report (self, ioc, timeout=None): |
| 992 | + """ |
| 993 | + Leverage multiprocessing to produce a single report for the supplied IP/domain indicator which includes data |
| 994 | + from: lookup, DFIdb, REPdb, and IOCdb. |
| 995 | +
|
| 996 | + :type ioc: str |
| 997 | + :param ioc: Indicator to lookup (IP, domain, URL) |
| 998 | + :type timeout: integer |
| 999 | + :param timeout: Maximum time given to producing the IOC report (default=60). |
| 1000 | +
|
| 1001 | + :rtype: dict |
| 1002 | + :return: API response. |
| 1003 | + """ |
| 1004 | + |
| 1005 | + # default timeout. |
| 1006 | + if timeout is None: |
| 1007 | + timeout = 60 |
| 1008 | + |
| 1009 | + # parallelization. |
| 1010 | + jobs = [] |
| 1011 | + mngr = multiprocessing.Manager() |
| 1012 | + resp = mngr.dict() |
| 1013 | + |
| 1014 | + # what kind of IOC are we dealing with. |
| 1015 | + if self.is_ip(ioc): |
| 1016 | + kind = "ip" |
| 1017 | + elif self.is_domain(ioc): |
| 1018 | + kind = "domain" |
| 1019 | + elif ioc.startswith("http"): |
| 1020 | + kind = "url" |
| 1021 | + else: |
| 1022 | + raise inquestlabs_exception("could not determine indicator type for %s" % ioc) |
| 1023 | + |
| 1024 | + # only IPs and domains get lookups. |
| 1025 | + if kind in ["ip", "domain"]: |
| 1026 | + job = multiprocessing.Process(target=worker_proxy, args=(self, "lookup", [kind, ioc], resp)) |
| 1027 | + jobs.append(job) |
| 1028 | + job.start() |
| 1029 | + |
| 1030 | + # all IOCs get compared against DFIdb, REPdb, and IOCdb |
| 1031 | + job = multiprocessing.Process(target=worker_proxy, args=(self, "dfi_search", ["ioc", kind, ioc], resp)) |
| 1032 | + jobs.append(job) |
| 1033 | + job.start() |
| 1034 | + |
| 1035 | + job = multiprocessing.Process(target=worker_proxy, args=(self, "repdb_search", [ioc], resp)) |
| 1036 | + jobs.append(job) |
| 1037 | + job.start() |
| 1038 | + |
| 1039 | + job = multiprocessing.Process(target=worker_proxy, args=(self, "iocdb_search", [ioc], resp)) |
| 1040 | + jobs.append(job) |
| 1041 | + job.start() |
| 1042 | + |
| 1043 | + # wait for jobs to complete. |
| 1044 | + self.__VERBOSE("waiting up to %d seconds for %d jobs to complete" % (timeout, len(jobs))) |
| 1045 | + |
| 1046 | + # wait for jobs to complete, up to timeout |
| 1047 | + start = time.time() |
| 1048 | + |
| 1049 | + while time.time() - start <= timeout: |
| 1050 | + if not any(job.is_alive() for job in jobs): |
| 1051 | + # all the processes are done, break now. |
| 1052 | + break |
| 1053 | + |
| 1054 | + # this prevents CPU hogging. |
| 1055 | + time.sleep(1) |
| 1056 | + |
| 1057 | + else: |
| 1058 | + self.__VERBOSE("timeout reached, killing jobs...") |
| 1059 | + for job in jobs: |
| 1060 | + job.terminate() |
| 1061 | + job.join() |
| 1062 | + |
| 1063 | + elapsed = time.time() - start |
| 1064 | + self.__VERBOSE("completed all jobs in %d seconds" % elapsed) |
| 1065 | + |
| 1066 | + # return the combined response. |
| 1067 | + return dict(resp) |
| 1068 | + |
| 1069 | + |
923 | 1070 | ####################################################################################################################
|
924 | 1071 | def stats (self):
|
925 | 1072 | """
|
@@ -1277,6 +1424,10 @@ def main ():
|
1277 | 1424 | else:
|
1278 | 1425 | raise inquestlabs_exception("'lookup' supports 'ip' and 'domain'.")
|
1279 | 1426 |
|
| 1427 | + ### IP/DOMAIN/URL REPORT ########################################################################################### |
| 1428 | + elif args['report']: |
| 1429 | + print(json.dumps(labs.report(args['<ioc>'], args['--timeout']))) |
| 1430 | + |
1280 | 1431 | ### MISCELLANEOUS ##################################################################################################
|
1281 | 1432 | elif args['stats']:
|
1282 | 1433 | print(json.dumps(labs.stats()))
|
|
0 commit comments