Skip to content

Commit c24483d

Browse files
author
azazelm3dj3d
authored
Merge pull request #60 from InQuest/dev
Resolving more community issues
2 parents 00ba370 + c9d2a1f commit c24483d

4 files changed

Lines changed: 125 additions & 42 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
build/
66
dist/
77
_build/
8+
docs-dist/

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# -- Project information -----------------------------------------------------
2121

2222
project = u'iocextract'
23-
copyright = u'2019 InQuest, LLC'
23+
copyright = u'2019 - 2023 InQuest, LLC'
2424
author = u'InQuest Labs'
2525

2626
# The short X.Y version

iocextract.py

Lines changed: 122 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -190,17 +190,35 @@ def url_re(open_end=False):
190190
HTTPS_SCHEME_DEFANG_RE = re.compile('hxxps', re.IGNORECASE)
191191

192192
# Get some valid obfuscated ip addresses.
193-
IPV4_RE = re.compile(r"""
194-
(?:^|
195-
(?![^\d\.])
196-
)
197-
(?:
193+
def ipv4_len(ip_len=3):
194+
if ip_len == 3:
195+
IPV4_RE = re.compile(r"""
196+
(?:^|
197+
(?![^\d\.])
198+
)
199+
(?:
200+
(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])
201+
[\[\(\\]*?\.[\]\)]*?
202+
){3}
203+
(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])
204+
(?:(?=[^\d\.])|$)
205+
""", re.VERBOSE)
206+
207+
elif ip_len == 4:
208+
IPV4_RE = re.compile(r"""
209+
(?:^|
210+
(?![^\d\.])
211+
)
212+
(?:
213+
(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])
214+
[\[\(\\]*?\.[\]\)]*?
215+
){4}
216+
([01]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])
198217
(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])
199-
[\[\(\\]*?\.[\]\)]*?
200-
){3}
201-
(?:[1-9]?\d|1\d\d|2[0-4]\d|25[0-5])
202-
(?:(?=[^\d\.])|$)
203-
""", re.VERBOSE)
218+
(?:(?=[^\d\.])|$)
219+
""", re.VERBOSE)
220+
221+
return IPV4_RE
204222

205223
# Experimental IPv6 regex, will not catch everything but should be sufficent for now.
206224
IPV6_RE = re.compile(r"""
@@ -210,6 +228,26 @@ def url_re(open_end=False):
210228
# Capture email addresses including common defangs.
211229
EMAIL_RE = re.compile(r"""
212230
(
231+
[a-z0-9_.+-]+
232+
[\(\[{\x20]*
233+
(?:
234+
(?:
235+
(?:
236+
\x20*
237+
""" + SEPARATOR_DEFANGS + r"""
238+
\x20*
239+
)*
240+
\.
241+
(?:
242+
\x20*
243+
""" + SEPARATOR_DEFANGS + r"""
244+
\x20*
245+
)*
246+
|
247+
\W+dot\W+
248+
)
249+
[a-z0-9-]+?
250+
)*
213251
[a-z0-9_.+-]+
214252
[\(\[{\x20]*
215253
(?:@|\Wat\W)
@@ -283,6 +321,7 @@ def extract_iocs(data, refang=False, strip=False):
283321
:param bool strip: Strip possible garbage from the end of URLs
284322
:rtype: :py:func:`itertools.chain`
285323
"""
324+
286325
return itertools.chain(
287326
extract_urls(data, refang=refang, strip=strip),
288327
extract_ips(data, refang=refang),
@@ -292,21 +331,22 @@ def extract_iocs(data, refang=False, strip=False):
292331
)
293332

294333

295-
def extract_urls(data, refang=False, strip=False, delimiter=None, open_punc=False):
334+
def extract_urls(data, refang=False, strip=False, delimiter=None, open_punc=False, no_scheme=False):
296335
"""Extract URLs.
297336
298337
:param data: Input text
299338
:param bool refang: Refang output?
300339
:param bool strip: Strip possible garbage from the end of URLs
301340
:rtype: :py:func:`itertools.chain`
302341
"""
342+
303343
return itertools.chain(
304-
extract_unencoded_urls(data, refang=refang, strip=strip, open_punc=open_punc),
344+
extract_unencoded_urls(data, refang=refang, strip=strip, open_punc=open_punc, no_scheme=no_scheme),
305345
extract_encoded_urls(data, refang=refang, strip=strip, delimiter=delimiter),
306346
)
307347

308348

309-
def extract_unencoded_urls(data, refang=False, strip=False, open_punc=False):
349+
def extract_unencoded_urls(data, refang=False, strip=False, open_punc=False, no_scheme=False):
310350
"""Extract only unencoded URLs.
311351
312352
:param data: Input text
@@ -323,7 +363,7 @@ def extract_unencoded_urls(data, refang=False, strip=False, open_punc=False):
323363

324364
for url in unencoded_urls:
325365
if refang:
326-
url = refang_url(url.group(1))
366+
url = refang_url(url.group(1), no_scheme=no_scheme)
327367
else:
328368
url = url.group(1)
329369

@@ -411,12 +451,29 @@ def extract_ipv4s(data, refang=False):
411451
:param bool refang: Refang output?
412452
:rtype: Iterator[:class:`str`]
413453
"""
414-
for ip_address in IPV4_RE.finditer(data):
454+
455+
def ipv4_str(data):
456+
protocol_str = re.compile(r"https|http|ftp")
457+
458+
for pro in protocol_str.finditer(data):
459+
if refang:
460+
return refang_ipv4(pro.group(0))
461+
else:
462+
return pro.group(0)
463+
464+
for ip_address in ipv4_len().finditer(data):
465+
# Iterates over any ip address with 4 numbers after the final (3rd) octet
466+
for ip_address in ipv4_len(4).finditer(data):
467+
pass
468+
415469
if refang:
416470
yield refang_ipv4(ip_address.group(0))
417471
else:
418472
yield ip_address.group(0)
419473

474+
if ipv4_str(data) != None:
475+
yield ipv4_str(data)
476+
420477

421478
def extract_ipv6s(data):
422479
"""Extract IPv6 addresses.
@@ -426,8 +483,11 @@ def extract_ipv6s(data):
426483
:param data: Input text
427484
:rtype: Iterator[:class:`str`]
428485
"""
486+
429487
for ip_address in IPV6_RE.finditer(data):
430-
yield ip_address.group(0)
488+
# Sets a minimal standard for IPv6 (0:0:0:0:0:0:0:0)
489+
if len(data) >= 15:
490+
yield ip_address.group(0)
431491

432492

433493
def extract_emails(data, refang=False):
@@ -437,7 +497,9 @@ def extract_emails(data, refang=False):
437497
:param bool refang: Refang output?
438498
:rtype: Iterator[:class:`str`]
439499
"""
500+
440501
for email in EMAIL_RE.finditer(data):
502+
441503
if refang:
442504
email = refang_email(email.group(1))
443505
else:
@@ -455,6 +517,7 @@ def extract_hashes(data):
455517
:param data: Input text
456518
:rtype: :py:func:`itertools.chain`
457519
"""
520+
458521
return itertools.chain(
459522
extract_md5_hashes(data),
460523
extract_sha1_hashes(data),
@@ -469,6 +532,7 @@ def extract_md5_hashes(data):
469532
:param data: Input text
470533
:rtype: Iterator[:class:`str`]
471534
"""
535+
472536
for md5 in MD5_RE.finditer(data):
473537
yield md5.group(1)
474538

@@ -479,6 +543,7 @@ def extract_sha1_hashes(data):
479543
:param data: Input text
480544
:rtype: Iterator[:class:`str`]
481545
"""
546+
482547
for sha1 in SHA1_RE.finditer(data):
483548
yield sha1.group(1)
484549

@@ -489,6 +554,7 @@ def extract_sha256_hashes(data):
489554
:param data: Input text
490555
:rtype: Iterator[:class:`str`]
491556
"""
557+
492558
for sha256 in SHA256_RE.finditer(data):
493559
yield sha256.group(1)
494560

@@ -499,6 +565,7 @@ def extract_sha512_hashes(data):
499565
:param data: Input text
500566
:rtype: Iterator[:class:`str`]
501567
"""
568+
502569
for sha512 in SHA512_RE.finditer(data):
503570
yield sha512.group(1)
504571

@@ -509,6 +576,7 @@ def extract_yara_rules(data):
509576
:param data: Input text
510577
:rtype: Iterator[:class:`str`]
511578
"""
579+
512580
for yara_rule in YARA_PARSE_RE.finditer(data):
513581
yield yara_rule.group(1).strip()
514582

@@ -546,6 +614,7 @@ def extract_custom_iocs(data, regex_list):
546614
:param regex_list: List of strings to treat as regex and match against data.
547615
:rtype: Iterator[:class:`str`]
548616
"""
617+
549618
# Compile all the regex strings first, so we can error out quickly.
550619
regex_objects = []
551620
for regex_string in regex_list:
@@ -563,6 +632,7 @@ def _is_ipv6_url(url):
563632
:param url: String URL
564633
:rtype: bool
565634
"""
635+
566636
# Fix urlparse exception.
567637
parsed = urlparse(url)
568638

@@ -586,6 +656,7 @@ def _refang_common(ioc):
586656
:param ioc: String IP/Email Address or URL netloc.
587657
:rtype: str
588658
"""
659+
589660
return ioc.replace('[dot]', '.').\
590661
replace('(dot)', '.').\
591662
replace('[.]', '.').\
@@ -601,6 +672,7 @@ def refang_email(email):
601672
:param email: String email address.
602673
:rtype: str
603674
"""
675+
604676
# Check for ' at ' and ' dot ' first.
605677
email = re.sub('\W[aA][tT]\W', '@', email.lower())
606678
email = re.sub('\W*[dD][oO][tT]\W*', '.', email)
@@ -609,16 +681,15 @@ def refang_email(email):
609681
return _refang_common(email).replace('[', '').\
610682
replace(']', '').\
611683
replace('{', '').\
612-
replace('}', '').\
613-
replace('{', '')
614-
684+
replace('}', '')
615685

616-
def refang_url(url):
686+
def refang_url(url, no_scheme=False):
617687
"""Refang a URL.
618688
619689
:param url: String URL
620690
:rtype: str
621691
"""
692+
622693
# First fix urlparse errors.
623694
# Fix ipv6 parsing exception.
624695
if '[.' in url and '[.]' not in url:
@@ -647,7 +718,8 @@ def refang_url(url):
647718
url = url.replace('\\\\', '//', 1)
648719
else:
649720
# Support no-protocol.
650-
url = 'http://' + url
721+
# url = 'http://' + url
722+
pass
651723

652724
# Refang (/), since it's not entirely in the netloc.
653725
url = url.replace('(/)', '/')
@@ -676,7 +748,10 @@ def refang_url(url):
676748
elif HTTPS_SCHEME_DEFANG_RE.fullmatch(parsed.scheme):
677749
scheme = 'https'
678750
else:
679-
scheme = 'http'
751+
if no_scheme:
752+
scheme = ''
753+
else:
754+
scheme = 'http'
680755

681756
parsed = parsed._replace(scheme=scheme)
682757
replacee = '{}:///'.format(scheme)
@@ -708,6 +783,7 @@ def refang_ipv4(ip_address):
708783
:param ip_address: String IPv4 address.
709784
:rtype: str
710785
"""
786+
711787
return _refang_common(ip_address).replace('[', '').\
712788
replace(']', '').\
713789
replace('\\', '')
@@ -719,6 +795,7 @@ def defang(ioc):
719795
:param ioc: String URL, domain, or IPv4 address.
720796
:rtype: str
721797
"""
798+
722799
# If it's a url, defang just the scheme and netloc.
723800
try:
724801
parsed = urlparse(ioc)
@@ -745,32 +822,34 @@ def main():
745822
description="""Advanced Indicator of Compromise (IOC) extractor.
746823
If no arguments are specified, the default behavior is
747824
to extract all IOCs.""")
748-
parser.add_argument('--input', type=lambda x: io.open(x, 'r', encoding='utf-8', errors='ignore'),
825+
parser.add_argument('-i', '--input', type=lambda x: io.open(x, 'r', encoding='utf-8', errors='ignore'),
749826
default=io.open(0, 'r', encoding='utf-8', errors='ignore'), help="default: stdin")
750-
parser.add_argument('--output', type=lambda x: io.open(x, 'w', encoding='utf-8', errors='ignore'),
827+
parser.add_argument('-o', '--output', type=lambda x: io.open(x, 'w', encoding='utf-8', errors='ignore'),
751828
default=io.open(1, 'w', encoding='utf-8', errors='ignore'), help="default: stdout")
752-
parser.add_argument('--extract-emails', action='store_true')
753-
parser.add_argument('--extract-ips', action='store_true')
754-
parser.add_argument('--extract-ipv4s', action='store_true')
755-
parser.add_argument('--extract-ipv6s', action='store_true')
756-
parser.add_argument('--extract-urls', action='store_true')
757-
parser.add_argument('--extract-yara-rules', action='store_true')
758-
parser.add_argument('--extract-hashes', action='store_true')
759-
parser.add_argument('--custom-regex', type=lambda x: io.open(x, 'r', encoding='utf-8', errors='ignore'),
829+
parser.add_argument('-ee', '--extract-emails', action='store_true')
830+
parser.add_argument('-ip', '--extract-ips', action='store_true')
831+
parser.add_argument('-ip4', '--extract-ipv4s', action='store_true')
832+
parser.add_argument('-ip6', '--extract-ipv6s', action='store_true')
833+
parser.add_argument('-u', '--extract-urls', action='store_true')
834+
parser.add_argument('-y', '--extract-yara-rules', action='store_true')
835+
parser.add_argument('-ha', '--extract-hashes', action='store_true')
836+
parser.add_argument('-cr', '--custom-regex', type=lambda x: io.open(x, 'r', encoding='utf-8', errors='ignore'),
760837
metavar='REGEX_FILE',
761838
help="file with custom regex strings, one per line, with one capture group each")
762-
parser.add_argument('--refang', action='store_true', help="default: no")
763-
parser.add_argument('--strip-urls', action='store_true',
839+
parser.add_argument('-r', '--refang', action='store_true', help="default: no")
840+
parser.add_argument('-su', '--strip-urls', action='store_true',
764841
help="remove possible garbage from the end of urls. default: no")
765-
parser.add_argument('--wide', action='store_true',
842+
parser.add_argument('-w', '--wide', action='store_true',
766843
help="preprocess input to allow wide-encoded character matches. default: no")
767-
parser.add_argument('--json', action='store_true')
768-
parser.add_argument('--open', action='store_true', help="Removes the end puncuation regex when extracting URLs")
844+
parser.add_argument('-j', '--json', action='store_true')
845+
parser.add_argument('-op', '--open', action='store_true', help="Removes the end puncuation regex when extracting URLs")
846+
parser.add_argument('-rm', '--rm_scheme', action='store_true', help="Removes the protocol from the url (i.e. http, https, etc.)")
769847

770848
args = parser.parse_args()
771849

772-
# Read input.
850+
# Read user unput
773851
data = args.input.read()
852+
774853
if args.wide:
775854
data = data.replace('\x00', '')
776855

@@ -785,6 +864,7 @@ def main():
785864
args.extract_emails or
786865
args.custom_regex
787866
)
867+
788868
memo = {}
789869

790870
if args.extract_emails or extract_all:
@@ -795,8 +875,10 @@ def main():
795875
memo["ipv6s"] = list(extract_ipv6s(data))
796876
if args.extract_urls or extract_all:
797877
memo["urls"] = list(extract_urls(data, refang=args.refang, strip=args.strip_urls))
798-
if args.extract_urls and args.open:
799-
memo["urls"] = list(extract_urls(data, refang=args.refang, strip=args.strip_urls, open_punc=True))
878+
if args.open:
879+
memo["open_punc"] = list(extract_urls(data, refang=args.refang, strip=args.strip_urls, open_punc=args.open))
880+
if args.rm_scheme:
881+
memo["no_protocol"] = list(extract_urls(data, refang=args.refang, strip=args.strip_urls, open_punc=args.open, no_scheme=args.rm_scheme))
800882
if args.extract_yara_rules or extract_all:
801883
memo["yara_rules"] = list(extract_yara_rules(data))
802884
if args.extract_hashes or extract_all:

0 commit comments

Comments
 (0)