iocscrape 0.2.2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {iocscrape-0.2.2 → iocscrape-0.2.3}/PKG-INFO +1 -1
- {iocscrape-0.2.2 → iocscrape-0.2.3}/iocscrape/cli.py +5 -3
- {iocscrape-0.2.2 → iocscrape-0.2.3}/pyproject.toml +1 -1
- iocscrape-0.2.3/tests/test_deobfuscate.py +34 -0
- iocscrape-0.2.3/tests/test_extract.py +53 -0
- iocscrape-0.2.3/tests/test_low_confidence.py +66 -0
- iocscrape-0.2.2/tests/test_smoke.py +0 -0
- {iocscrape-0.2.2 → iocscrape-0.2.3}/.gitignore +0 -0
- {iocscrape-0.2.2 → iocscrape-0.2.3}/LICENSE +0 -0
- {iocscrape-0.2.2 → iocscrape-0.2.3}/README.md +0 -0
- {iocscrape-0.2.2 → iocscrape-0.2.3}/iocscrape/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iocscrape
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: CTI tool to extract IOCs from CTI reports (URLs or files), and write them to an output file. Low-confidence items are grouped at the end.
|
|
5
5
|
Author-email: Faisal Albuloushi <SQL@hotmail.co.uk>
|
|
6
6
|
License: The MIT License (MIT)
|
|
@@ -24,7 +24,7 @@ from publicsuffix2 import get_sld
|
|
|
24
24
|
# ========= #
|
|
25
25
|
|
|
26
26
|
APP_NAME = "iocscrape"
|
|
27
|
-
APP_VERSION = "0.2.
|
|
27
|
+
APP_VERSION = "0.2.3"
|
|
28
28
|
PROJECT_URL = "https://github.com/fwalbuloushi/iocscrape"
|
|
29
29
|
APP_DESC = "CTI tool to extract IOCs from CTI reports (URLs or files), and write them to an output file. Low-confidence items are grouped at the end."
|
|
30
30
|
|
|
@@ -502,13 +502,15 @@ class WarninglistIndex:
|
|
|
502
502
|
if d in self._exact_map:
|
|
503
503
|
return self._exact_map[d]
|
|
504
504
|
|
|
505
|
-
# 2. Check parent domains (suffix match)
|
|
505
|
+
# 2. Check parent domains (suffix match) — skip bare TLDs
|
|
506
506
|
parts = d.split(".")
|
|
507
507
|
for i in range(1, len(parts)):
|
|
508
508
|
parent = ".".join(parts[i:])
|
|
509
|
+
if "." not in parent:
|
|
510
|
+
continue
|
|
509
511
|
if parent in self._exact_map:
|
|
510
512
|
return self._exact_map[parent]
|
|
511
|
-
|
|
513
|
+
|
|
512
514
|
return None
|
|
513
515
|
|
|
514
516
|
def match_url(self, url: str) -> Optional[str]:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "iocscrape"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "CTI tool to extract IOCs from CTI reports (URLs or files), and write them to an output file. Low-confidence items are grouped at the end."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from iocscrape.cli import deobfuscate_text
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_hxxps():
|
|
5
|
+
assert deobfuscate_text("hxxps://evil.com") == "https://evil.com"
|
|
6
|
+
|
|
7
|
+
def test_hxxp():
|
|
8
|
+
assert deobfuscate_text("hxxp://evil.com") == "http://evil.com"
|
|
9
|
+
|
|
10
|
+
def test_dot_bracket():
|
|
11
|
+
assert deobfuscate_text("evil[.]com") == "evil.com"
|
|
12
|
+
|
|
13
|
+
def test_dot_paren():
|
|
14
|
+
assert deobfuscate_text("evil(.)com") == "evil.com"
|
|
15
|
+
|
|
16
|
+
def test_dot_brace():
|
|
17
|
+
assert deobfuscate_text("evil{.}com") == "evil.com"
|
|
18
|
+
|
|
19
|
+
def test_dot_word():
|
|
20
|
+
assert deobfuscate_text("evil dot com") == "evil.com"
|
|
21
|
+
|
|
22
|
+
def test_dot_word_with_spaces():
|
|
23
|
+
assert deobfuscate_text("evil (dot) com") == "evil.com"
|
|
24
|
+
|
|
25
|
+
def test_colon_bracket():
|
|
26
|
+
assert deobfuscate_text("hxxps[://]evil.com") == "https://evil.com"
|
|
27
|
+
|
|
28
|
+
def test_mixed():
|
|
29
|
+
result = deobfuscate_text("hxxps://evil[.]com/path")
|
|
30
|
+
assert result == "https://evil.com/path"
|
|
31
|
+
|
|
32
|
+
def test_no_change():
|
|
33
|
+
original = "https://legitimate.com"
|
|
34
|
+
assert deobfuscate_text(original) == original
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from iocscrape.cli import extract_iocs
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_url():
|
|
5
|
+
iocs = extract_iocs("Payload was downloaded from https://malware.example.com/payload")
|
|
6
|
+
assert "https://malware.example.com/payload" in iocs["url"]
|
|
7
|
+
|
|
8
|
+
def test_domain():
|
|
9
|
+
iocs = extract_iocs("The threat actor used evil.example.com as a C2 server")
|
|
10
|
+
assert "evil.example.com" in iocs["domain"]
|
|
11
|
+
|
|
12
|
+
def test_ipv4():
|
|
13
|
+
iocs = extract_iocs("Beacon traffic was observed to 192.168.1.100 on port 443")
|
|
14
|
+
assert "192.168.1.100" in iocs["ipv4"]
|
|
15
|
+
|
|
16
|
+
def test_ipv6():
|
|
17
|
+
iocs = extract_iocs("C2 communication detected to 2001:db8::1 over port 8443")
|
|
18
|
+
assert "2001:db8::1" in iocs["ipv6"]
|
|
19
|
+
|
|
20
|
+
def test_email():
|
|
21
|
+
iocs = extract_iocs("Spear-phishing email was sent from attacker@evil.com")
|
|
22
|
+
assert "attacker@evil.com" in iocs["email"]
|
|
23
|
+
|
|
24
|
+
def test_md5():
|
|
25
|
+
iocs = extract_iocs("Malware sample identified with MD5: d41d8cd98f00b204e9800998ecf8427e")
|
|
26
|
+
assert "d41d8cd98f00b204e9800998ecf8427e" in iocs["md5"]
|
|
27
|
+
|
|
28
|
+
def test_sha1():
|
|
29
|
+
iocs = extract_iocs("Dropper hash SHA1: da39a3ee5e6b4b0d3255bfef95601890afd80709")
|
|
30
|
+
assert "da39a3ee5e6b4b0d3255bfef95601890afd80709" in iocs["sha1"]
|
|
31
|
+
|
|
32
|
+
def test_sha256():
|
|
33
|
+
iocs = extract_iocs("Ransomware binary SHA256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855")
|
|
34
|
+
assert "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" in iocs["sha256"]
|
|
35
|
+
|
|
36
|
+
def test_cve():
|
|
37
|
+
iocs = extract_iocs("The exploit targets CVE-2021-44228 to achieve remote code execution")
|
|
38
|
+
assert "CVE-2021-44228" in iocs["cve"]
|
|
39
|
+
|
|
40
|
+
def test_sha256_not_in_sha1_or_md5():
|
|
41
|
+
"""A 64-char hash should only appear in sha256, not sha1 or md5."""
|
|
42
|
+
h = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
|
43
|
+
iocs = extract_iocs(h)
|
|
44
|
+
assert h in iocs["sha256"]
|
|
45
|
+
assert h not in iocs["sha1"]
|
|
46
|
+
assert h not in iocs["md5"]
|
|
47
|
+
|
|
48
|
+
def test_sha1_not_in_md5():
|
|
49
|
+
"""A 40-char hash should only appear in sha1, not md5."""
|
|
50
|
+
h = "da39a3ee5e6b4b0d3255bfef95601890afd80709"
|
|
51
|
+
iocs = extract_iocs(h)
|
|
52
|
+
assert h in iocs["sha1"]
|
|
53
|
+
assert h not in iocs["md5"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from iocscrape.cli import compute_low_confidence, load_warninglists, WarninglistIndex
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _wl():
|
|
5
|
+
return load_warninglists()
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_filename_like_domain_flagged():
|
|
9
|
+
iocs = {"domain": {"malware.png"}, "url": set(), "ipv4": set(), "ipv6": set(), "email": set(), "md5": set(), "sha1": set(), "sha256": set(), "cve": set()}
|
|
10
|
+
low = compute_low_confidence(iocs, _wl())
|
|
11
|
+
values = [item.value for item in low]
|
|
12
|
+
assert "malware.png" in values
|
|
13
|
+
|
|
14
|
+
def test_static_asset_url_flagged():
|
|
15
|
+
iocs = {"domain": set(), "url": {"https://cdn.example.com/style.css"}, "ipv4": set(), "ipv6": set(), "email": set(), "md5": set(), "sha1": set(), "sha256": set(), "cve": set()}
|
|
16
|
+
low = compute_low_confidence(iocs, _wl())
|
|
17
|
+
values = [item.value for item in low]
|
|
18
|
+
assert "https://cdn.example.com/style.css" in values
|
|
19
|
+
|
|
20
|
+
def test_private_ipv4_flagged():
|
|
21
|
+
iocs = {"domain": set(), "url": set(), "ipv4": {"10.0.0.1"}, "ipv6": set(), "email": set(), "md5": set(), "sha1": set(), "sha256": set(), "cve": set()}
|
|
22
|
+
low = compute_low_confidence(iocs, _wl())
|
|
23
|
+
values = [item.value for item in low]
|
|
24
|
+
assert "10.0.0.1" in values
|
|
25
|
+
|
|
26
|
+
def test_loopback_ipv4_flagged():
|
|
27
|
+
iocs = {"domain": set(), "url": set(), "ipv4": {"127.0.0.1"}, "ipv6": set(), "email": set(), "md5": set(), "sha1": set(), "sha256": set(), "cve": set()}
|
|
28
|
+
low = compute_low_confidence(iocs, _wl())
|
|
29
|
+
values = [item.value for item in low]
|
|
30
|
+
assert "127.0.0.1" in values
|
|
31
|
+
|
|
32
|
+
def test_warninglist_domain_is_flagged():
|
|
33
|
+
"""A domain that is explicitly in the warninglist should be flagged as low-confidence."""
|
|
34
|
+
wl = WarninglistIndex()
|
|
35
|
+
wl._exact_map["google.com"] = "Test warninglist"
|
|
36
|
+
iocs = {"domain": {"google.com"}, "url": set(), "ipv4": set(), "ipv6": set(), "email": set(), "md5": set(), "sha1": set(), "sha256": set(), "cve": set()}
|
|
37
|
+
low = compute_low_confidence(iocs, wl)
|
|
38
|
+
values = [item.value for item in low]
|
|
39
|
+
assert "google.com" in values
|
|
40
|
+
|
|
41
|
+
def test_subdomain_of_warninglist_domain_is_flagged():
|
|
42
|
+
"""A subdomain of a warninglist entry should be flagged via suffix matching."""
|
|
43
|
+
wl = WarninglistIndex()
|
|
44
|
+
wl._exact_map["mitre.org"] = "Test warninglist"
|
|
45
|
+
iocs = {"domain": {"sub.mitre.org"}, "url": set(), "ipv4": set(), "ipv6": set(), "email": set(), "md5": set(), "sha1": set(), "sha256": set(), "cve": set()}
|
|
46
|
+
low = compute_low_confidence(iocs, wl)
|
|
47
|
+
values = [item.value for item in low]
|
|
48
|
+
assert "sub.mitre.org" in values
|
|
49
|
+
|
|
50
|
+
def test_bare_tld_in_warninglist_does_not_flag_all_domains():
|
|
51
|
+
"""A bare TLD entry in the warninglist (e.g. 'com') must not cause every
|
|
52
|
+
.com domain to be flagged as low-confidence via suffix matching."""
|
|
53
|
+
wl = WarninglistIndex()
|
|
54
|
+
wl._exact_map["com"] = "IANA TLDs"
|
|
55
|
+
iocs = {"domain": {"evil-c2.com"}, "url": set(), "ipv4": set(), "ipv6": set(), "email": set(), "md5": set(), "sha1": set(), "sha256": set(), "cve": set()}
|
|
56
|
+
low = compute_low_confidence(iocs, wl)
|
|
57
|
+
values = [item.value for item in low]
|
|
58
|
+
assert "evil-c2.com" not in values
|
|
59
|
+
|
|
60
|
+
def test_ioc_domain_not_false_positive():
|
|
61
|
+
"""A real C2-style IOC domain whose parent is not in any warninglist
|
|
62
|
+
should stay high-confidence and not be moved to low-confidence."""
|
|
63
|
+
iocs = {"domain": {"c2-server.badactor-infra.com"}, "url": set(), "ipv4": set(), "ipv6": set(), "email": set(), "md5": set(), "sha1": set(), "sha256": set(), "cve": set()}
|
|
64
|
+
low = compute_low_confidence(iocs, _wl())
|
|
65
|
+
values = [item.value for item in low]
|
|
66
|
+
assert "c2-server.badactor-infra.com" not in values
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|