threatpulse-cli 1.0.0__py3-none-any.whl

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.
@@ -0,0 +1,17 @@
1
+ """ThreatPulse - Fast, modular Threat Intelligence & IOC enrichment engine for SOC Analysts."""
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "Aleksei Zharkou"
5
+ __license__ = "MIT"
6
+
7
+ from threat_pulse.models import IOCType, RiskLevel, EnrichmentResult
8
+ from threat_pulse.detector import detect_ioc_type
9
+ from threat_pulse.scorer import calculate_risk_score
10
+
11
+ __all__ = [
12
+ "IOCType",
13
+ "RiskLevel",
14
+ "EnrichmentResult",
15
+ "detect_ioc_type",
16
+ "calculate_risk_score",
17
+ ]
@@ -0,0 +1,7 @@
1
+ """Execution wrapper for python -m threat_pulse."""
2
+
3
+ import sys
4
+ from threat_pulse.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
threat_pulse/cache.py ADDED
@@ -0,0 +1,119 @@
1
+ """Local SQLite caching layer for threat intelligence queries."""
2
+
3
+ import json
4
+ import sqlite3
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Optional, Dict, Any
8
+ from threat_pulse.models import EnrichmentResult, IOCType, RiskLevel
9
+
10
+
11
+ class ThreatCache:
12
+ """Persistent local cache for IOC enrichment results to respect API rate limits."""
13
+
14
+ def __init__(self, db_path: Optional[str] = None, ttl_seconds: int = 86400):
15
+ if db_path:
16
+ self.db_path = Path(db_path)
17
+ else:
18
+ self.db_path = Path.home() / ".threatpulse_cache.db"
19
+ self.ttl = ttl_seconds
20
+ self._init_db()
21
+
22
+ def _init_db(self) -> None:
23
+ """Create cache table if not already existing."""
24
+ try:
25
+ conn = sqlite3.connect(self.db_path)
26
+ try:
27
+ cursor = conn.cursor()
28
+ cursor.execute(
29
+ """
30
+ CREATE TABLE IF NOT EXISTS ioc_cache (
31
+ ioc TEXT PRIMARY KEY,
32
+ ioc_type TEXT,
33
+ data_json TEXT,
34
+ cached_at INTEGER
35
+ )
36
+ """
37
+ )
38
+ conn.commit()
39
+ finally:
40
+ conn.close()
41
+ except Exception:
42
+ pass
43
+
44
+ def get(self, ioc: str) -> Optional[EnrichmentResult]:
45
+ """Retrieve cached result if within TTL."""
46
+ try:
47
+ conn = sqlite3.connect(self.db_path)
48
+ try:
49
+ cursor = conn.cursor()
50
+ cursor.execute(
51
+ "SELECT ioc_type, data_json, cached_at FROM ioc_cache WHERE ioc = ?",
52
+ (ioc.lower().strip(),)
53
+ )
54
+ row = cursor.fetchone()
55
+ if not row:
56
+ return None
57
+
58
+ ioc_type_str, data_json, cached_at = row
59
+ if time.time() - cached_at > self.ttl:
60
+ return None # Expired
61
+
62
+ payload = json.loads(data_json)
63
+ payload["cached"] = True
64
+ return EnrichmentResult(
65
+ ioc=payload["ioc"],
66
+ ioc_type=IOCType(payload["ioc_type"]),
67
+ defanged=payload["defanged"],
68
+ risk_score=payload["risk_score"],
69
+ risk_level=RiskLevel(payload["risk_level"]),
70
+ verdict=payload["verdict"],
71
+ is_private=payload.get("is_private", False),
72
+ is_bogon=payload.get("is_bogon", False),
73
+ tags=payload.get("tags", []),
74
+ mitre_attack=payload.get("mitre_attack", []),
75
+ details=payload.get("details", {}),
76
+ recommendation=payload.get("recommendation", ""),
77
+ cached=True,
78
+ timestamp=payload.get("timestamp", "")
79
+ )
80
+ finally:
81
+ conn.close()
82
+ except Exception:
83
+ return None
84
+
85
+ def set(self, result: EnrichmentResult) -> None:
86
+ """Save enrichment result into local SQLite cache."""
87
+ try:
88
+ payload_json = json.dumps(result.to_dict())
89
+ conn = sqlite3.connect(self.db_path)
90
+ try:
91
+ cursor = conn.cursor()
92
+ cursor.execute(
93
+ """
94
+ INSERT OR REPLACE INTO ioc_cache (ioc, ioc_type, data_json, cached_at)
95
+ VALUES (?, ?, ?, ?)
96
+ """,
97
+ (result.ioc.lower().strip(), result.ioc_type.value, payload_json, int(time.time()))
98
+ )
99
+ conn.commit()
100
+ finally:
101
+ conn.close()
102
+ except Exception:
103
+ pass
104
+
105
+ def clear(self) -> int:
106
+ """Clear all entries from cache and return count of deleted records."""
107
+ try:
108
+ conn = sqlite3.connect(self.db_path)
109
+ try:
110
+ cursor = conn.cursor()
111
+ cursor.execute("SELECT count(*) FROM ioc_cache")
112
+ count = cursor.fetchone()[0]
113
+ cursor.execute("DELETE FROM ioc_cache")
114
+ conn.commit()
115
+ return count
116
+ finally:
117
+ conn.close()
118
+ except Exception:
119
+ return 0
threat_pulse/cli.py ADDED
@@ -0,0 +1,282 @@
1
+ """Command Line Interface (CLI) entry point for ThreatPulse."""
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import List, Optional
8
+
9
+ from threat_pulse import __version__
10
+ from threat_pulse.cache import ThreatCache
11
+ from threat_pulse.detector import detect_ioc_type, defang, refang, extract_iocs_from_text
12
+ from threat_pulse.enrichers.abuseipdb import AbuseIPDBEnricher
13
+ from threat_pulse.enrichers.malware import MalwareHashEnricher
14
+ from threat_pulse.enrichers.network import NetworkEnricher
15
+ from threat_pulse.formatters import (
16
+ render_terminal_card,
17
+ render_terminal_summary_table,
18
+ render_json,
19
+ render_markdown,
20
+ render_wazuh_cdb_list,
21
+ )
22
+ from threat_pulse.heuristics import (
23
+ analyze_ip_heuristics,
24
+ analyze_domain_heuristics,
25
+ analyze_url_heuristics,
26
+ )
27
+ from threat_pulse.models import EnrichmentResult, IOCType, RiskLevel
28
+ from threat_pulse.scorer import calculate_risk_score
29
+
30
+
31
+ def enrich_single_ioc(
32
+ raw_ioc: str,
33
+ cache: Optional[ThreatCache] = None,
34
+ abuse_api_key: Optional[str] = None,
35
+ use_cache: bool = True
36
+ ) -> EnrichmentResult:
37
+ """Enrich a single IOC across all heuristic, network, and intelligence engines."""
38
+ clean_target = refang(raw_ioc).strip()
39
+ ioc_type = detect_ioc_type(clean_target)
40
+
41
+ # 1. Check Cache
42
+ if use_cache and cache:
43
+ cached_result = cache.get(clean_target)
44
+ if cached_result:
45
+ return cached_result
46
+
47
+ # 2. Run Heuristics
48
+ heuristics = {}
49
+ if ioc_type in (IOCType.IPV4, IOCType.IPV6):
50
+ heuristics = analyze_ip_heuristics(clean_target)
51
+ elif ioc_type == IOCType.DOMAIN:
52
+ heuristics = analyze_domain_heuristics(clean_target)
53
+ elif ioc_type == IOCType.URL:
54
+ heuristics = analyze_url_heuristics(clean_target)
55
+
56
+ # 3. Run Enrichers
57
+ network_enricher = NetworkEnricher()
58
+ abuse_enricher = AbuseIPDBEnricher(api_key=abuse_api_key)
59
+ malware_enricher = MalwareHashEnricher()
60
+
61
+ net_data = network_enricher.enrich(clean_target, ioc_type) if network_enricher.can_enrich(ioc_type) else {}
62
+ abuse_data = abuse_enricher.enrich(clean_target, ioc_type) if abuse_enricher.can_enrich(ioc_type) else {}
63
+ malware_data = malware_enricher.enrich(clean_target, ioc_type) if malware_enricher.can_enrich(ioc_type) else {}
64
+
65
+ # 4. Score and Classify
66
+ score, level, verdict, tags, mitre, recom = calculate_risk_score(
67
+ clean_target, ioc_type, heuristics, net_data, abuse_data, malware_data
68
+ )
69
+
70
+ # Combine all details
71
+ combined_details = {**heuristics, **net_data, **abuse_data, **malware_data}
72
+
73
+ result = EnrichmentResult(
74
+ ioc=clean_target,
75
+ ioc_type=ioc_type,
76
+ defanged=defang(clean_target),
77
+ risk_score=score,
78
+ risk_level=level,
79
+ verdict=verdict,
80
+ is_private=heuristics.get("is_rfc1918", False),
81
+ is_bogon=heuristics.get("is_bogon", False),
82
+ tags=tags,
83
+ mitre_attack=mitre,
84
+ details=combined_details,
85
+ recommendation=recom,
86
+ cached=False,
87
+ )
88
+
89
+ # Save to Cache
90
+ if use_cache and cache:
91
+ cache.set(result)
92
+
93
+ return result
94
+
95
+
96
+ def build_parser() -> argparse.ArgumentParser:
97
+ """Build the CLI argument parser with comprehensive options."""
98
+ parser = argparse.ArgumentParser(
99
+ prog="threat-pulse",
100
+ description="ThreatPulse - Fast Threat Intelligence & IOC Enrichment CLI for SOC Analysts.",
101
+ formatter_class=argparse.RawDescriptionHelpFormatter,
102
+ epilog="""
103
+ Examples:
104
+ threat-pulse 185.220.101.5
105
+ threat-pulse 84c82835a5d21bbcf75a61706d8ab549
106
+ threat-pulse evil-dga-domain192837.top --format json
107
+ threat-pulse --file access.log --format table
108
+ threat-pulse --file suspicious_iocs.txt --format wazuh-cdb --out threat_pulse.cdb
109
+ threat-pulse --defang http://malicious.com/payload.exe
110
+ """
111
+ )
112
+ parser.add_argument(
113
+ "ioc",
114
+ nargs="?",
115
+ help="Target IOC to inspect (IPv4, IPv6, Domain, URL, MD5, SHA1, SHA256)"
116
+ )
117
+ parser.add_argument(
118
+ "-f", "--file",
119
+ help="Path to file (log, alert export, text) containing IOCs to extract and analyze"
120
+ )
121
+ parser.add_argument(
122
+ "--format",
123
+ choices=["card", "table", "json", "markdown", "wazuh-cdb"],
124
+ default="card",
125
+ help="Output visualization format (default: card for single, table for batch)"
126
+ )
127
+ parser.add_argument(
128
+ "-o", "--out",
129
+ help="Write report to an output file instead of stdout"
130
+ )
131
+ parser.add_argument(
132
+ "--api-key",
133
+ help="AbuseIPDB API key (defaults to ABUSEIPDB_API_KEY environment variable)"
134
+ )
135
+ parser.add_argument(
136
+ "--no-cache",
137
+ action="store_true",
138
+ help="Bypass local SQLite cache and force fresh enrichment"
139
+ )
140
+ parser.add_argument(
141
+ "--clear-cache",
142
+ action="store_true",
143
+ help="Wipe the local ThreatPulse SQLite cache and exit"
144
+ )
145
+ parser.add_argument(
146
+ "--defang",
147
+ help="Quickly defang an indicator and print the result"
148
+ )
149
+ parser.add_argument(
150
+ "--refang",
151
+ help="Quickly refang an indicator and print the canonical form"
152
+ )
153
+ parser.add_argument(
154
+ "-v", "--version",
155
+ action="version",
156
+ version=f"ThreatPulse v{__version__}"
157
+ )
158
+ return parser
159
+
160
+
161
+ def main() -> int:
162
+ # Ensure Windows console supports UTF-8 Unicode characters (borders, bullets, emoji)
163
+ if sys.platform == "win32":
164
+ try:
165
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
166
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
167
+ except Exception:
168
+ pass
169
+
170
+ parser = build_parser()
171
+ args = parser.parse_args()
172
+
173
+ cache = ThreatCache()
174
+
175
+ # Clear cache action
176
+ if args.clear_cache:
177
+ deleted = cache.clear()
178
+ print(f"[*] ThreatPulse cache cleared successfully ({deleted} entries removed).")
179
+ return 0
180
+
181
+ # Quick defang / refang helpers
182
+ if args.defang:
183
+ print(defang(args.defang))
184
+ return 0
185
+
186
+ if args.refang:
187
+ print(refang(args.refang))
188
+ return 0
189
+
190
+ use_cache = not args.no_cache
191
+ iocs_to_check: List[str] = []
192
+
193
+ # Single IOC from positional argument
194
+ if args.ioc:
195
+ iocs_to_check.append(args.ioc)
196
+
197
+ # Batch IOCs from file
198
+ if args.file:
199
+ file_path = Path(args.file)
200
+ if not file_path.is_file():
201
+ print(f"[!] Error: File '{args.file}' not found.", file=sys.stderr)
202
+ return 1
203
+ try:
204
+ content = file_path.read_text(encoding="utf-8", errors="ignore")
205
+ extracted = extract_iocs_from_text(content)
206
+ for val, _ in extracted:
207
+ if val not in iocs_to_check:
208
+ iocs_to_check.append(val)
209
+ except Exception as e:
210
+ print(f"[!] Error reading file: {e}", file=sys.stderr)
211
+ return 1
212
+
213
+ if not iocs_to_check:
214
+ parser.print_help()
215
+ return 1
216
+
217
+ # Enrich all targets (parallelized for batch files)
218
+ results: List[EnrichmentResult] = []
219
+ if len(iocs_to_check) > 1:
220
+ import concurrent.futures
221
+ future_map = {}
222
+ with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
223
+ for target in iocs_to_check:
224
+ fut = executor.submit(
225
+ enrich_single_ioc,
226
+ target,
227
+ cache=cache,
228
+ abuse_api_key=args.api_key,
229
+ use_cache=use_cache
230
+ )
231
+ future_map[fut] = target
232
+
233
+ completed_map = {}
234
+ for fut in concurrent.futures.as_completed(future_map):
235
+ t = future_map[fut]
236
+ completed_map[t] = fut.result()
237
+
238
+ # Maintain original order of inputs
239
+ results = [completed_map[t] for t in iocs_to_check if t in completed_map]
240
+ else:
241
+ results = [
242
+ enrich_single_ioc(
243
+ iocs_to_check[0],
244
+ cache=cache,
245
+ abuse_api_key=args.api_key,
246
+ use_cache=use_cache
247
+ )
248
+ ]
249
+
250
+ # Determine default format
251
+ selected_format = args.format
252
+ if len(results) > 1 and selected_format == "card" and not args.out:
253
+ selected_format = "table"
254
+
255
+ output_text = ""
256
+ if selected_format == "card":
257
+ output_text = "\n\n".join(render_terminal_card(r) for r in results)
258
+ elif selected_format == "table":
259
+ output_text = render_terminal_summary_table(results)
260
+ elif selected_format == "json":
261
+ output_text = render_json(results, pretty=True)
262
+ elif selected_format == "markdown":
263
+ output_text = render_markdown(results)
264
+ elif selected_format == "wazuh-cdb":
265
+ output_text = render_wazuh_cdb_list(results)
266
+
267
+ # Write output or print to console
268
+ if args.out:
269
+ try:
270
+ Path(args.out).write_text(output_text, encoding="utf-8")
271
+ print(f"[+] Threat report successfully written to {args.out}")
272
+ except Exception as e:
273
+ print(f"[!] Error writing to {args.out}: {e}", file=sys.stderr)
274
+ return 1
275
+ else:
276
+ print(output_text)
277
+
278
+ return 0
279
+
280
+
281
+ if __name__ == "__main__":
282
+ sys.exit(main())
@@ -0,0 +1,136 @@
1
+ """Indicator of Compromise (IOC) type detection and extraction engine."""
2
+
3
+ import ipaddress
4
+ import re
5
+ from typing import List, Tuple
6
+ from threat_pulse.models import IOCType
7
+
8
+ # Regular expressions for IOC detection
9
+ IPV4_REGEX = re.compile(
10
+ r"\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"
11
+ r"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b"
12
+ )
13
+
14
+ MD5_REGEX = re.compile(r"\b[a-fA-F0-9]{32}\b")
15
+ SHA1_REGEX = re.compile(r"\b[a-fA-F0-9]{40}\b")
16
+ SHA256_REGEX = re.compile(r"\b[a-fA-F0-9]{64}\b")
17
+
18
+ # Matches standard domains (2+ labels, valid TLD length)
19
+ DOMAIN_REGEX = re.compile(
20
+ r"\b(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,24}\b"
21
+ )
22
+
23
+ # Matches URLs (http, https, ftp, or defanged hxxp/hxxps)
24
+ URL_REGEX = re.compile(
25
+ r"\b(?:https?|hxxps?|ftp)://[^\s/$.?#].[^\s]*", re.IGNORECASE
26
+ )
27
+
28
+
29
+ def refang(ioc: str) -> str:
30
+ """Normalize defanged indicators back into canonical format."""
31
+ cleaned = ioc.strip()
32
+ # Replace bracketed dots: [.] or (.) or {.} -> .
33
+ cleaned = re.sub(r"[\[\(\{]\.[\]\)\}]", ".", cleaned)
34
+ # Replace hxxp/hxxps -> http/https
35
+ cleaned = re.sub(r"^hxxp(s?):", r"http\1:", cleaned, flags=re.IGNORECASE)
36
+ # Replace [:] -> :
37
+ cleaned = re.sub(r"\[:\]", ":", cleaned)
38
+ return cleaned
39
+
40
+
41
+ def defang(ioc: str) -> str:
42
+ """Defang an indicator to prevent accidental clicks or resolutions."""
43
+ cleaned = ioc.strip()
44
+ # Defang http/https
45
+ cleaned = re.sub(r"^http:", "hxxp:", cleaned, flags=re.IGNORECASE)
46
+ cleaned = re.sub(r"^https:", "hxxps:", cleaned, flags=re.IGNORECASE)
47
+ # Defang dots
48
+ return cleaned.replace(".", "[.]")
49
+
50
+
51
+ def detect_ioc_type(raw_ioc: str) -> IOCType:
52
+ """Classify an IOC string into a specific IOCType."""
53
+ target = refang(raw_ioc).strip()
54
+
55
+ # 1. URL Check
56
+ if URL_REGEX.match(target) or target.startswith(("http://", "https://", "ftp://")):
57
+ return IOCType.URL
58
+
59
+ # 2. IP Checks
60
+ try:
61
+ ip = ipaddress.ip_address(target)
62
+ if ip.version == 4:
63
+ return IOCType.IPV4
64
+ elif ip.version == 6:
65
+ return IOCType.IPV6
66
+ except ValueError:
67
+ pass
68
+
69
+ # 3. Hash Checks
70
+ if SHA256_REGEX.fullmatch(target):
71
+ return IOCType.SHA256
72
+ if SHA1_REGEX.fullmatch(target):
73
+ return IOCType.SHA1
74
+ if MD5_REGEX.fullmatch(target):
75
+ return IOCType.MD5
76
+
77
+ # 4. Domain Check
78
+ if DOMAIN_REGEX.fullmatch(target):
79
+ # Exclude common file extensions that match domain regex by accident (e.g. script.sh, tool.py)
80
+ excluded_suffixes = {
81
+ "py", "sh", "exe", "dll", "bat", "ps1", "json", "xml", "csv", "log",
82
+ "txt", "md", "yml", "yaml", "tar", "gz", "zip", "rar", "7z", "conf"
83
+ }
84
+ parts = target.lower().split(".")
85
+ if len(parts) == 2 and parts[-1] in excluded_suffixes:
86
+ return IOCType.UNKNOWN
87
+ return IOCType.DOMAIN
88
+
89
+ return IOCType.UNKNOWN
90
+
91
+
92
+ def extract_iocs_from_text(text: str) -> List[Tuple[str, IOCType]]:
93
+ """Scan raw text/logs and extract all recognized indicators."""
94
+ results: List[Tuple[str, IOCType]] = []
95
+ seen = set()
96
+
97
+ # 1. URLs
98
+ for match in URL_REGEX.finditer(text):
99
+ val = refang(match.group(0))
100
+ if val not in seen:
101
+ seen.add(val)
102
+ results.append((val, IOCType.URL))
103
+
104
+ # 2. IP Addresses (v4)
105
+ for match in IPV4_REGEX.finditer(text):
106
+ val = match.group(0)
107
+ try:
108
+ ipaddress.IPv4Address(val)
109
+ if val not in seen:
110
+ seen.add(val)
111
+ results.append((val, IOCType.IPV4))
112
+ except ValueError:
113
+ continue
114
+
115
+ # 3. Hashes
116
+ for match in SHA256_REGEX.finditer(text):
117
+ val = match.group(0)
118
+ if val not in seen:
119
+ seen.add(val)
120
+ results.append((val, IOCType.SHA256))
121
+
122
+ for match in MD5_REGEX.finditer(text):
123
+ val = match.group(0)
124
+ if val not in seen:
125
+ seen.add(val)
126
+ results.append((val, IOCType.MD5))
127
+
128
+ # 4. Domains (avoid matching substrings of already extracted URLs or IPs)
129
+ for match in DOMAIN_REGEX.finditer(text):
130
+ val = match.group(0)
131
+ if detect_ioc_type(val) == IOCType.DOMAIN:
132
+ if val not in seen:
133
+ seen.add(val)
134
+ results.append((val, IOCType.DOMAIN))
135
+
136
+ return results
@@ -0,0 +1,13 @@
1
+ """Enrichers package initialization."""
2
+
3
+ from threat_pulse.enrichers.base import BaseEnricher
4
+ from threat_pulse.enrichers.network import NetworkEnricher
5
+ from threat_pulse.enrichers.abuseipdb import AbuseIPDBEnricher
6
+ from threat_pulse.enrichers.malware import MalwareHashEnricher
7
+
8
+ __all__ = [
9
+ "BaseEnricher",
10
+ "NetworkEnricher",
11
+ "AbuseIPDBEnricher",
12
+ "MalwareHashEnricher",
13
+ ]
@@ -0,0 +1,69 @@
1
+ """AbuseIPDB Threat Intelligence API client with graceful offline fallback."""
2
+
3
+ import json
4
+ import os
5
+ import urllib.request
6
+ import urllib.error
7
+ from typing import Dict, Any, Optional
8
+ from threat_pulse.enrichers.base import BaseEnricher
9
+ from threat_pulse.models import IOCType
10
+
11
+
12
+ class AbuseIPDBEnricher(BaseEnricher):
13
+ """Enriches IP addresses using AbuseIPDB APIv2 or fallback heuristics."""
14
+
15
+ def __init__(self, api_key: Optional[str] = None, timeout: float = 3.0):
16
+ super().__init__(name="abuseipdb")
17
+ self.api_key = api_key or os.getenv("ABUSEIPDB_API_KEY")
18
+ self.timeout = timeout
19
+
20
+ def can_enrich(self, ioc_type: IOCType) -> bool:
21
+ return ioc_type in (IOCType.IPV4, IOCType.IPV6)
22
+
23
+ def enrich(self, ioc: str, ioc_type: IOCType) -> Dict[str, Any]:
24
+ data: Dict[str, Any] = {
25
+ "queried": False,
26
+ "abuse_score": 0,
27
+ "total_reports": 0,
28
+ "country_code": "UNKNOWN",
29
+ "usage_type": "Unknown",
30
+ "isp": "Unknown",
31
+ "is_whitelisted": False,
32
+ "last_reported_at": None,
33
+ "categories": [],
34
+ "error": None,
35
+ }
36
+
37
+ if not self.api_key:
38
+ data["error"] = "No API key configured (using local heuristics). Set ABUSEIPDB_API_KEY to enable live checks."
39
+ return data
40
+
41
+ url = f"https://api.abuseipdb.com/api/v2/check?ipAddress={ioc}&maxAgeInDays=90&verbose"
42
+ headers = {
43
+ "Key": self.api_key,
44
+ "Accept": "application/json",
45
+ "User-Agent": "ThreatPulse-SOC-CLI/1.0"
46
+ }
47
+
48
+ try:
49
+ req = urllib.request.Request(url, headers=headers, method="GET")
50
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
51
+ if resp.status == 200:
52
+ payload = json.loads(resp.read().decode("utf-8"))
53
+ report = payload.get("data", {})
54
+ data["queried"] = True
55
+ data["abuse_score"] = report.get("abuseConfidenceScore", 0)
56
+ data["total_reports"] = report.get("totalReports", 0)
57
+ data["country_code"] = report.get("countryCode", "UNKNOWN")
58
+ data["usage_type"] = report.get("usageType", "Unknown")
59
+ data["isp"] = report.get("isp", "Unknown")
60
+ data["is_whitelisted"] = report.get("isWhitelisted", False)
61
+ data["last_reported_at"] = report.get("lastReportedAt")
62
+ except urllib.error.HTTPError as e:
63
+ data["error"] = f"HTTP {e.code}: {e.reason}"
64
+ except urllib.error.URLError as e:
65
+ data["error"] = f"Network error: {e.reason}"
66
+ except Exception as e:
67
+ data["error"] = str(e)
68
+
69
+ return data
@@ -0,0 +1,23 @@
1
+ """Base classes for threat intelligence enrichers."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, Any, Optional
5
+ from threat_pulse.models import IOCType
6
+
7
+
8
+ class BaseEnricher(ABC):
9
+ """Abstract interface for all threat intelligence and metadata enrichers."""
10
+
11
+ def __init__(self, name: str, enabled: bool = True):
12
+ self.name = name
13
+ self.enabled = enabled
14
+
15
+ @abstractmethod
16
+ def can_enrich(self, ioc_type: IOCType) -> bool:
17
+ """Determine if this enricher supports the given IOC type."""
18
+ pass
19
+
20
+ @abstractmethod
21
+ def enrich(self, ioc: str, ioc_type: IOCType) -> Dict[str, Any]:
22
+ """Perform enrichment and return structured metadata dictionary."""
23
+ pass