Skip to content

Commit 645f311

Browse files
Merge pull request #6 from InQuest/parallel-ioc-report
New parallelized IOC report.
2 parents 412566e + 63140e5 commit 645f311

File tree

1 file changed

+156
-5
lines changed

1 file changed

+156
-5
lines changed

inquestlabs.py

Lines changed: 156 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
inquestlabs [options] yara widere <regex> [(--big-endian|--little-endian)]
2626
inquestlabs [options] lookup ip <ioc>
2727
inquestlabs [options] lookup domain <ioc>
28+
inquestlabs [options] report <ioc>
2829
inquestlabs [options] stats
2930
inquestlabs [options] setup <apikey>
3031
inquestlabs [options] trystero list-days
@@ -44,6 +45,7 @@
4445
--little-endian Toggle little endian.
4546
--offset=<offset> Specify an offset other than 0 for the trigger.
4647
--proxy=<proxy> Intermediate proxy
48+
--timeout=<timeout> Maximum amount of time to wait for IOC report.
4749
--verbose=<level> Verbosity level, outputs to stderr [default: 0].
4850
--version Show version.
4951
"""
@@ -68,6 +70,8 @@
6870
pass
6971

7072
# standard libraries.
73+
import multiprocessing
74+
import ipaddress
7175
import hashlib
7276
import random
7377
import time
@@ -76,17 +80,27 @@
7680
import os
7781
import re
7882

79-
__version__ = 1.0
83+
__version__ = 1.1
8084

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-_]+")
8590

8691
# verbosity levels.
8792
INFO = 1
8893
DEBUG = 2
8994

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+
90104
########################################################################################################################
91105
class inquestlabs_exception(Exception):
92106
pass
@@ -814,6 +828,59 @@ def iocdb_sources (self):
814828

815829
return self.API("/iocdb/sources")
816830

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+
817884
####################################################################################################################
818885
def lookup (self, kind, ioc):
819886
"""
@@ -920,6 +987,86 @@ def repdb_sources (self):
920987

921988
return self.API("/repdb/sources")
922989

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+
9231070
####################################################################################################################
9241071
def stats (self):
9251072
"""
@@ -1277,6 +1424,10 @@ def main ():
12771424
else:
12781425
raise inquestlabs_exception("'lookup' supports 'ip' and 'domain'.")
12791426

1427+
### IP/DOMAIN/URL REPORT ###########################################################################################
1428+
elif args['report']:
1429+
print(json.dumps(labs.report(args['<ioc>'], args['--timeout'])))
1430+
12801431
### MISCELLANEOUS ##################################################################################################
12811432
elif args['stats']:
12821433
print(json.dumps(labs.stats()))

0 commit comments

Comments
 (0)