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,80 @@
1
+ """Malware hash intelligence enricher with offline threat signature database."""
2
+
3
+ from typing import Dict, Any, Optional
4
+ from threat_pulse.enrichers.base import BaseEnricher
5
+ from threat_pulse.models import IOCType
6
+
7
+ # Curated offline database of critical malware hashes (Ransomware, Mimikatz, C2 Beacons, WebShells)
8
+ KNOWN_MALWARE_HASHES: Dict[str, Dict[str, str]] = {
9
+ # WannaCry Ransomware
10
+ "ed01ebf83334a19373140c0df0ddb638ec6d7e99f8788f5b8978cb8c4cb5ffce": {
11
+ "family": "WannaCry Ransomware",
12
+ "type": "Ransomware / Cryptoworm",
13
+ "severity": "CRITICAL",
14
+ "description": "EternalBlue-based ransomware targeting SMBv1 (MS17-010)"
15
+ },
16
+ "84c82835a5d21bbcf75a61706d8ab549": {
17
+ "family": "WannaCry Dropper",
18
+ "type": "Dropper",
19
+ "severity": "CRITICAL",
20
+ "description": "WannaCry primary dropper binary"
21
+ },
22
+ # Mimikatz (Credential Dumping)
23
+ "a60c52bb53a5c0b89cf53dc5bb7c2c129219b16e4ee7d9c6e3b5e43a9b1c9d2f": {
24
+ "family": "Mimikatz",
25
+ "type": "Credential Dumper",
26
+ "severity": "CRITICAL",
27
+ "description": "LSASS memory credential extraction utility (T1003.001)"
28
+ },
29
+ # Cobalt Strike Beacon
30
+ "4d01b1bcf03ebdd8cb0667b9381665a58d6ff8cb194fa8cbdf24dd3ee3228a9b": {
31
+ "family": "Cobalt Strike",
32
+ "type": "Adversary Simulation / C2 Beacon",
33
+ "severity": "CRITICAL",
34
+ "description": "Malleable C2 Beacon executable (T1071)"
35
+ },
36
+ # Emotet Trojan
37
+ "49b788647055a40bbd21b79313ea5fa2e6ff3f1b": {
38
+ "family": "Emotet",
39
+ "type": "Banking Trojan / Botnet",
40
+ "severity": "HIGH",
41
+ "description": "Polymorphic modular botnet loader"
42
+ },
43
+ # China Chopper / Classic WebShell
44
+ "8bc6e6f96602330a6c0c2bb01c56ad44": {
45
+ "family": "China Chopper WebShell",
46
+ "type": "Web Shell",
47
+ "severity": "HIGH",
48
+ "description": "Minimalist server-side web backdoor (T1505.003)"
49
+ }
50
+ }
51
+
52
+
53
+ class MalwareHashEnricher(BaseEnricher):
54
+ """Enriches file hashes against known malware signatures and IOC threat feeds."""
55
+
56
+ def __init__(self):
57
+ super().__init__(name="malware_hash_enricher")
58
+
59
+ def can_enrich(self, ioc_type: IOCType) -> bool:
60
+ return ioc_type in (IOCType.MD5, IOCType.SHA1, IOCType.SHA256)
61
+
62
+ def enrich(self, ioc: str, ioc_type: IOCType) -> Dict[str, Any]:
63
+ normalized_hash = ioc.lower().strip()
64
+ data: Dict[str, Any] = {
65
+ "is_known_malware": False,
66
+ "family": None,
67
+ "type": None,
68
+ "severity": None,
69
+ "description": None,
70
+ }
71
+
72
+ if normalized_hash in KNOWN_MALWARE_HASHES:
73
+ intel = KNOWN_MALWARE_HASHES[normalized_hash]
74
+ data["is_known_malware"] = True
75
+ data["family"] = intel["family"]
76
+ data["type"] = intel["type"]
77
+ data["severity"] = intel["severity"]
78
+ data["description"] = intel["description"]
79
+
80
+ return data
@@ -0,0 +1,81 @@
1
+ """Network enricher: DNS PTR resolution, Tor exit node detection, and scanner identification."""
2
+
3
+ import socket
4
+ from typing import Dict, Any, Set
5
+ from threat_pulse.enrichers.base import BaseEnricher
6
+ from threat_pulse.models import IOCType
7
+
8
+ # Known active Tor exit nodes (sample curated offline seed list for instant high-confidence matching)
9
+ KNOWN_TOR_EXIT_NODES: Set[str] = {
10
+ "185.220.101.5", "185.220.101.6", "185.220.101.7",
11
+ "185.220.100.241", "185.220.100.242", "185.220.100.252",
12
+ "195.176.3.23", "51.15.43.205", "185.107.56.210",
13
+ "109.70.100.25", "185.14.29.178", "198.96.155.3"
14
+ }
15
+
16
+ # Known security research / port scanner subnets & IPs
17
+ KNOWN_SCANNERS: Dict[str, str] = {
18
+ "198.20.69.74": "Shadowserver Foundation Scanner",
19
+ "198.20.69.98": "Shadowserver Foundation Scanner",
20
+ "162.142.125.0": "Censys Research Scanner",
21
+ "167.248.133.0": "Censys Research Scanner",
22
+ "71.6.232.0": "Shodan Internet Scanner",
23
+ "71.6.165.200": "Shodan Internet Scanner",
24
+ "185.180.143.0": "LeakIX Security Scanner",
25
+ }
26
+
27
+
28
+ class NetworkEnricher(BaseEnricher):
29
+ """Enriches network indicators with reverse DNS, Tor status, and scanner attribution."""
30
+
31
+ def __init__(self, timeout: float = 2.0):
32
+ super().__init__(name="network_enricher")
33
+ self.timeout = timeout
34
+
35
+ def can_enrich(self, ioc_type: IOCType) -> bool:
36
+ return ioc_type in (IOCType.IPV4, IOCType.IPV6, IOCType.DOMAIN)
37
+
38
+ def enrich(self, ioc: str, ioc_type: IOCType) -> Dict[str, Any]:
39
+ data: Dict[str, Any] = {
40
+ "ptr": None,
41
+ "resolved_ips": [],
42
+ "is_tor_exit": False,
43
+ "is_known_scanner": False,
44
+ "scanner_name": None,
45
+ }
46
+
47
+ # IP Specific Network Checks
48
+ if ioc_type in (IOCType.IPV4, IOCType.IPV6):
49
+ # Check Tor
50
+ if ioc in KNOWN_TOR_EXIT_NODES:
51
+ data["is_tor_exit"] = True
52
+
53
+ # Check Known Scanners
54
+ for scanner_ip, scanner_name in KNOWN_SCANNERS.items():
55
+ if ioc == scanner_ip or ioc.startswith(scanner_ip.rsplit(".", 1)[0]):
56
+ data["is_known_scanner"] = True
57
+ data["scanner_name"] = scanner_name
58
+ break
59
+
60
+ # Reverse DNS (PTR)
61
+ try:
62
+ socket.setdefaulttimeout(self.timeout)
63
+ host_info = socket.gethostbyaddr(ioc)
64
+ data["ptr"] = host_info[0]
65
+ except (socket.herror, socket.gaierror, socket.timeout, OSError):
66
+ data["ptr"] = None
67
+
68
+ # Domain Specific Network Checks
69
+ elif ioc_type == IOCType.DOMAIN:
70
+ try:
71
+ socket.setdefaulttimeout(self.timeout)
72
+ _, _, ip_list = socket.gethostbyname_ex(ioc)
73
+ data["resolved_ips"] = ip_list
74
+ # Check if domain resolves to a Tor node or scanner
75
+ for ip in ip_list:
76
+ if ip in KNOWN_TOR_EXIT_NODES:
77
+ data["is_tor_exit"] = True
78
+ except (socket.gaierror, socket.timeout, OSError):
79
+ data["resolved_ips"] = []
80
+
81
+ return data
@@ -0,0 +1,140 @@
1
+ """Terminal card, table, JSON, Markdown, and Wazuh CDB list output formatters."""
2
+
3
+ import json
4
+ from typing import List
5
+ from threat_pulse.models import EnrichmentResult, RiskLevel
6
+
7
+ # ANSI Color codes for clean, zero-dependency colored terminal rendering
8
+ RESET = "\033[0m"
9
+ BOLD = "\033[1m"
10
+ DIM = "\033[2m"
11
+ RED = "\033[91m"
12
+ GREEN = "\033[92m"
13
+ YELLOW = "\033[93m"
14
+ BLUE = "\033[94m"
15
+ MAGENTA = "\033[95m"
16
+ CYAN = "\033[96m"
17
+ WHITE = "\033[97m"
18
+ BG_RED = "\033[41m"
19
+ BG_YELLOW = "\033[43m"
20
+ BG_GREEN = "\033[42m"
21
+
22
+
23
+ def _risk_color(level: RiskLevel) -> str:
24
+ if level == RiskLevel.CRITICAL:
25
+ return f"{BG_RED}{WHITE}{BOLD}"
26
+ elif level == RiskLevel.HIGH:
27
+ return f"{RED}{BOLD}"
28
+ elif level == RiskLevel.MEDIUM:
29
+ return f"{YELLOW}{BOLD}"
30
+ elif level == RiskLevel.LOW:
31
+ return f"{BLUE}"
32
+ else:
33
+ return f"{GREEN}{BOLD}"
34
+
35
+
36
+ def render_terminal_card(res: EnrichmentResult) -> str:
37
+ """Render a standalone, highly detailed threat intelligence card for a single IOC."""
38
+ color = _risk_color(res.risk_level)
39
+ lines = []
40
+ lines.append(f"{CYAN}┌────────────────────────────────────────────────────────────────────────┐{RESET}")
41
+ lines.append(f"{CYAN}│{RESET} {BOLD}ThreatPulse IOC Report{RESET} {DIM}[{res.ioc_type.value.upper()}]{RESET}")
42
+ lines.append(f"{CYAN}├────────────────────────────────────────────────────────────────────────┤{RESET}")
43
+ lines.append(f"{CYAN}│{RESET} {BOLD}Indicator:{RESET} {res.defanged} {DIM}(raw: {res.ioc}){RESET}")
44
+ lines.append(f"{CYAN}│{RESET} {BOLD}Risk Level:{RESET} {color} {res.risk_level.value} ({res.risk_score}/100) {RESET}")
45
+ lines.append(f"{CYAN}│{RESET} {BOLD}Verdict:{RESET} {res.verdict}")
46
+
47
+ if res.tags:
48
+ tag_str = " ".join([f"{MAGENTA}#{t}{RESET}" for t in res.tags])
49
+ lines.append(f"{CYAN}│{RESET} {BOLD}Tags:{RESET} {tag_str}")
50
+
51
+ if res.mitre_attack:
52
+ lines.append(f"{CYAN}│{RESET} {BOLD}MITRE ATT&CK:{RESET}")
53
+ for m in res.mitre_attack:
54
+ lines.append(f"{CYAN}│{RESET} • {YELLOW}{m}{RESET}")
55
+
56
+ # Specific details section
57
+ details = res.details
58
+ if details:
59
+ lines.append(f"{CYAN}│{RESET} {BOLD}Intelligence Details:{RESET}")
60
+ if "ptr" in details and details["ptr"]:
61
+ lines.append(f"{CYAN}│{RESET} • Reverse DNS: {details['ptr']}")
62
+ if "country_code" in details and details["country_code"] != "UNKNOWN":
63
+ lines.append(f"{CYAN}│{RESET} • Country / ISP: {details['country_code']} ({details.get('isp', 'N/A')})")
64
+ if "abuse_score" in details and details["abuse_score"] > 0:
65
+ lines.append(f"{CYAN}│{RESET} • AbuseIPDB Score: {details['abuse_score']}% ({details.get('total_reports', 0)} reports)")
66
+ if "family" in details and details["family"]:
67
+ lines.append(f"{CYAN}│{RESET} • Malware Family: {details['family']} ({details.get('description', '')})")
68
+ if "sld_entropy" in details:
69
+ lines.append(f"{CYAN}│{RESET} • Domain Entropy: {details['sld_entropy']} (DGA Candidate: {details.get('is_dga_candidate')})")
70
+ if "is_known_scanner" in details and details["is_known_scanner"]:
71
+ lines.append(f"{CYAN}│{RESET} • Scanner: {details.get('scanner_name')}")
72
+
73
+ lines.append(f"{CYAN}│{RESET} {BOLD}SOC Action:{RESET} {WHITE}{res.recommendation}{RESET}")
74
+ cache_badge = f"{DIM}(from local cache){RESET}" if res.cached else f"{DIM}(live analysis){RESET}"
75
+ lines.append(f"{CYAN}│{RESET} {DIM}Timestamp: {res.timestamp} {cache_badge}{RESET}")
76
+ lines.append(f"{CYAN}└────────────────────────────────────────────────────────────────────────┘{RESET}")
77
+ return "\n".join(lines)
78
+
79
+
80
+ def render_terminal_summary_table(results: List[EnrichmentResult]) -> str:
81
+ """Render a clean summary table for batch IOC enrichment."""
82
+ if not results:
83
+ return "No IOCs analyzed."
84
+
85
+ headers = f"{'INDICATOR':<35} {'TYPE':<8} {'RISK':<12} {'SCORE':<7} {'VERDICT':<30}"
86
+ separator = "-" * 95
87
+ lines = [f"{BOLD}{headers}{RESET}", separator]
88
+
89
+ for r in results:
90
+ color = _risk_color(r.risk_level)
91
+ short_verdict = (r.verdict[:27] + "...") if len(r.verdict) > 30 else r.verdict
92
+ line = (
93
+ f"{r.defanged[:33]:<35} "
94
+ f"{r.ioc_type.value:<8} "
95
+ f"{color}{r.risk_level.value:<12}{RESET} "
96
+ f"{r.risk_score:<7} "
97
+ f"{short_verdict:<30}"
98
+ )
99
+ lines.append(line)
100
+
101
+ return "\n".join(lines)
102
+
103
+
104
+ def render_json(results: List[EnrichmentResult], pretty: bool = True) -> str:
105
+ """Serialize results list to JSON."""
106
+ data = [r.to_dict() for r in results]
107
+ indent = 2 if pretty else None
108
+ return json.dumps({"count": len(results), "indicators": data}, indent=indent)
109
+
110
+
111
+ def render_markdown(results: List[EnrichmentResult]) -> str:
112
+ """Format results into a Markdown table for ticketing / GitHub / reporting."""
113
+ lines = [
114
+ "# ThreatPulse Threat Intelligence Report",
115
+ "",
116
+ f"**Total Indicators Analyzed:** {len(results)}",
117
+ "",
118
+ "| Indicator (Defanged) | Type | Risk Level | Score | Verdict | Tags |",
119
+ "| --------------------- | ---- | ---------- | ----- | ------- | ---- |"
120
+ ]
121
+ for r in results:
122
+ tags_str = ", ".join(r.tags) if r.tags else "-"
123
+ lines.append(
124
+ f"| `{r.defanged}` | {r.ioc_type.value} | **{r.risk_level.value}** | {r.risk_score}/100 | {r.verdict} | {tags_str} |"
125
+ )
126
+ return "\n".join(lines)
127
+
128
+
129
+ def render_wazuh_cdb_list(results: List[EnrichmentResult]) -> str:
130
+ """
131
+ Format malicious/suspicious indicators into Wazuh CDB list syntax.
132
+ Syntax: <key>:<value>
133
+ Usage: save to /var/ossec/etc/lists/threat_pulse.cdb
134
+ """
135
+ lines = ["# ThreatPulse exported CDB List for Wazuh SIEM"]
136
+ for r in results:
137
+ if r.risk_score >= 40: # Medium, High, or Critical
138
+ tag_summary = ",".join(r.tags[:2]) if r.tags else "malicious"
139
+ lines.append(f"{r.ioc}:{r.risk_level.value}_{r.risk_score}:{tag_summary}")
140
+ return "\n".join(lines)
@@ -0,0 +1,167 @@
1
+ """Offline heuristic analyzers for IP addresses, domains, and URLs."""
2
+
3
+ import ipaddress
4
+ import math
5
+ import re
6
+ from typing import Dict, Any, Optional, Tuple
7
+ from urllib.parse import urlparse
8
+
9
+ # Suspicious TLDs frequently abused in malware campaigns / phishing
10
+ HIGH_ABUSE_TLDS = {
11
+ "top", "xyz", "tk", "ml", "ga", "cf", "gq", "buzz", "icu", "work",
12
+ "click", "link", "rest", "surf", "monster", "casa", "country", "kim"
13
+ }
14
+
15
+ # Bogon / Reserved network ranges (IPv4)
16
+ BOGON_NETWORKS = [
17
+ ipaddress.ip_network("0.0.0.0/8"),
18
+ ipaddress.ip_network("10.0.0.0/8"), # RFC 1918
19
+ ipaddress.ip_network("100.64.0.0/10"), # Carrier-grade NAT
20
+ ipaddress.ip_network("127.0.0.0/8"), # Loopback
21
+ ipaddress.ip_network("169.254.0.0/16"), # Link-local / APIPA
22
+ ipaddress.ip_network("172.16.0.0/12"), # RFC 1918
23
+ ipaddress.ip_network("192.0.0.0/24"), # IETF Protocol
24
+ ipaddress.ip_network("192.0.2.0/24"), # TEST-NET-1
25
+ ipaddress.ip_network("192.168.0.0/16"), # RFC 1918
26
+ ipaddress.ip_network("198.18.0.0/15"), # Network Interconnect Benchmarking
27
+ ipaddress.ip_network("198.51.100.0/24"), # TEST-NET-2
28
+ ipaddress.ip_network("203.0.113.0/24"), # TEST-NET-3
29
+ ipaddress.ip_network("224.0.0.0/4"), # Multicast
30
+ ipaddress.ip_network("240.0.0.0/4"), # Reserved / Future use
31
+ ipaddress.ip_network("255.255.255.255/32"),# Broadcast
32
+ ]
33
+
34
+
35
+ def calculate_shannon_entropy(text: str) -> float:
36
+ """Calculate Shannon entropy of a string to evaluate randomness (DGA detection)."""
37
+ if not text:
38
+ return 0.0
39
+ entropy = 0.0
40
+ text_len = len(text)
41
+ freq: Dict[str, int] = {}
42
+ for char in text:
43
+ freq[char] = freq.get(char, 0) + 1
44
+ for count in freq.values():
45
+ p = count / text_len
46
+ entropy -= p * math.log2(p)
47
+ return round(entropy, 3)
48
+
49
+
50
+ def analyze_ip_heuristics(ip_str: str) -> Dict[str, Any]:
51
+ """Inspect IP against RFC1918, Bogon, Loopback, and Reserved spaces."""
52
+ try:
53
+ ip = ipaddress.ip_address(ip_str)
54
+ except ValueError:
55
+ return {"valid": False}
56
+
57
+ is_private = ip.is_private
58
+ is_loopback = ip.is_loopback
59
+ is_link_local = ip.is_link_local
60
+ is_multicast = ip.is_multicast
61
+ is_reserved = ip.is_reserved
62
+
63
+ is_bogon = False
64
+ bogon_reason = ""
65
+ if ip.version == 4:
66
+ for net in BOGON_NETWORKS:
67
+ if ip in net:
68
+ is_bogon = True
69
+ bogon_reason = str(net)
70
+ break
71
+
72
+ rfc1918 = False
73
+ if ip.version == 4:
74
+ rfc_nets = [
75
+ ipaddress.ip_network("10.0.0.0/8"),
76
+ ipaddress.ip_network("172.16.0.0/12"),
77
+ ipaddress.ip_network("192.168.0.0/16"),
78
+ ]
79
+ rfc1918 = any(ip in net for net in rfc_nets)
80
+
81
+ return {
82
+ "valid": True,
83
+ "version": ip.version,
84
+ "is_private": is_private,
85
+ "is_rfc1918": rfc1918,
86
+ "is_bogon": is_bogon,
87
+ "bogon_network": bogon_reason,
88
+ "is_loopback": is_loopback,
89
+ "is_link_local": is_link_local,
90
+ "is_multicast": is_multicast,
91
+ "is_reserved": is_reserved,
92
+ }
93
+
94
+
95
+ def analyze_domain_heuristics(domain: str) -> Dict[str, Any]:
96
+ """Inspect domain for DGA entropy, suspicious TLDs, and IDN homograph punycode."""
97
+ clean_domain = domain.lower().strip()
98
+ parts = clean_domain.split(".")
99
+
100
+ tld = parts[-1] if len(parts) > 1 else ""
101
+ sld = parts[-2] if len(parts) > 1 else parts[0]
102
+
103
+ # Calculate entropy on Second-Level Domain (the main name)
104
+ sld_entropy = calculate_shannon_entropy(sld)
105
+
106
+ is_high_entropy = (sld_entropy >= 3.6 and len(sld) >= 8) or (sld_entropy >= 3.3 and len(sld) >= 14)
107
+ has_suspicious_tld = tld in HIGH_ABUSE_TLDS
108
+ is_punycode = "xn--" in clean_domain
109
+ has_subdomain_abuse = len(parts) >= 4
110
+
111
+ # Count digits in main domain name (common in botnets / DGA)
112
+ digit_count = sum(c.isdigit() for c in sld)
113
+ digit_ratio = digit_count / len(sld) if sld else 0.0
114
+
115
+ return {
116
+ "domain": clean_domain,
117
+ "sld": sld,
118
+ "tld": tld,
119
+ "sld_entropy": sld_entropy,
120
+ "is_dga_candidate": is_high_entropy or (digit_ratio >= 0.4 and len(sld) >= 8),
121
+ "suspicious_tld": has_suspicious_tld,
122
+ "is_punycode": is_punycode,
123
+ "subdomain_depth": len(parts),
124
+ "excessive_subdomains": has_subdomain_abuse,
125
+ "digit_ratio": round(digit_ratio, 2),
126
+ }
127
+
128
+
129
+ def analyze_url_heuristics(url_str: str) -> Dict[str, Any]:
130
+ """Inspect URL structure for raw IP hosts, suspicious paths, or credentials in URI."""
131
+ try:
132
+ parsed = urlparse(url_str)
133
+ except Exception:
134
+ return {"valid": False}
135
+
136
+ hostname = parsed.hostname or ""
137
+ path = parsed.path or ""
138
+ query = parsed.query or ""
139
+
140
+ is_raw_ip = False
141
+ try:
142
+ if hostname:
143
+ ipaddress.ip_address(hostname)
144
+ is_raw_ip = True
145
+ except ValueError:
146
+ is_raw_ip = False
147
+
148
+ has_credentials = bool(parsed.username or parsed.password)
149
+ has_executable_extension = any(
150
+ path.lower().endswith(ext)
151
+ for ext in [".exe", ".dll", ".ps1", ".vbs", ".bat", ".scr", ".iso", ".hta", ".apk"]
152
+ )
153
+ uncommon_port = parsed.port not in [None, 80, 443, 8080]
154
+
155
+ domain_data = analyze_domain_heuristics(hostname) if not is_raw_ip else {}
156
+
157
+ return {
158
+ "valid": True,
159
+ "scheme": parsed.scheme,
160
+ "hostname": hostname,
161
+ "port": parsed.port,
162
+ "uncommon_port": uncommon_port,
163
+ "is_raw_ip_host": is_raw_ip,
164
+ "has_credentials": has_credentials,
165
+ "has_executable_extension": has_executable_extension,
166
+ "domain_heuristics": domain_data,
167
+ }
threat_pulse/models.py ADDED
@@ -0,0 +1,67 @@
1
+ """Data models and enums for ThreatPulse."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import Any, Dict, List, Optional
6
+ import datetime
7
+
8
+
9
+ class IOCType(str, Enum):
10
+ """Supported indicator of compromise types."""
11
+ IPV4 = "ipv4"
12
+ IPV6 = "ipv6"
13
+ DOMAIN = "domain"
14
+ URL = "url"
15
+ MD5 = "md5"
16
+ SHA1 = "sha1"
17
+ SHA256 = "sha256"
18
+ UNKNOWN = "unknown"
19
+
20
+
21
+ class RiskLevel(str, Enum):
22
+ """Categorized threat risk level based on aggregated score."""
23
+ CLEAN = "CLEAN" # 0 - 15
24
+ LOW = "LOW" # 16 - 39
25
+ MEDIUM = "MEDIUM" # 40 - 69
26
+ HIGH = "HIGH" # 70 - 89
27
+ CRITICAL = "CRITICAL" # 90 - 100
28
+
29
+
30
+ @dataclass
31
+ class EnrichmentResult:
32
+ """Consolidated threat intelligence report for a single IOC."""
33
+ ioc: str
34
+ ioc_type: IOCType
35
+ defanged: str
36
+ risk_score: int
37
+ risk_level: RiskLevel
38
+ verdict: str
39
+ is_private: bool = False
40
+ is_bogon: bool = False
41
+ tags: List[str] = field(default_factory=list)
42
+ mitre_attack: List[str] = field(default_factory=list)
43
+ details: Dict[str, Any] = field(default_factory=dict)
44
+ recommendation: str = ""
45
+ cached: bool = False
46
+ timestamp: str = field(
47
+ default_factory=lambda: datetime.datetime.now(datetime.timezone.utc).isoformat()
48
+ )
49
+
50
+ def to_dict(self) -> Dict[str, Any]:
51
+ """Convert result to a serializable dictionary."""
52
+ return {
53
+ "ioc": self.ioc,
54
+ "ioc_type": self.ioc_type.value,
55
+ "defanged": self.defanged,
56
+ "risk_score": self.risk_score,
57
+ "risk_level": self.risk_level.value,
58
+ "verdict": self.verdict,
59
+ "is_private": self.is_private,
60
+ "is_bogon": self.is_bogon,
61
+ "tags": self.tags,
62
+ "mitre_attack": self.mitre_attack,
63
+ "details": self.details,
64
+ "recommendation": self.recommendation,
65
+ "cached": self.cached,
66
+ "timestamp": self.timestamp,
67
+ }