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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iocscrape
3
- Version: 0.2.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.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.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