|
1 | | -import shodan |
2 | 1 | from bbot.modules.base import BaseModule |
3 | 2 |
|
4 | 3 |
|
5 | 4 | class shodan_enterprise(BaseModule): |
6 | 5 | watched_events = ["IP_ADDRESS"] |
7 | 6 | produced_events = ["OPEN_TCP_PORT", "TECHNOLOGY", "OPEN_UDP_PORT", "ASN", "VULNERABILITY"] |
8 | | - flags = ["passive"] |
| 7 | + flags = ["passive", "safe"] |
9 | 8 | meta = { |
10 | 9 | "created_date": "2026-01-27", |
11 | 10 | "author": "@Control-Punk-Delete", |
12 | 11 | "description": "Shodan Enterprise API integration module.", |
| 12 | + "auth_required": True, |
| 13 | + } |
| 14 | + options = {"api_key": "", "in_scope_only": True} |
| 15 | + options_desc = { |
| 16 | + "api_key": "Shodan API Key", |
| 17 | + "in_scope_only": "Only query in-scope IPs. If False, will query up to distance 1.", |
13 | 18 | } |
14 | | - deps_pip = ["shodan"] |
15 | | - options = {"api_key": None} |
16 | | - options_desc = {"api_key": "Shodan API Key"} |
17 | | - per_host_only = True |
18 | 19 | scope_distance_modifier = 1 |
19 | | - target_only = True |
| 20 | + |
| 21 | + base_url = "https://api.shodan.io" |
20 | 22 |
|
21 | 23 | async def setup(self): |
22 | | - if not self.config.get("api_key"): |
| 24 | + self.api_key = self.config.get("api_key", "") |
| 25 | + if not self.api_key: |
23 | 26 | return None, "No API key specified" |
24 | | - self.api_key = self.config.get("api_key") |
| 27 | + if not self.config.get("in_scope_only", True): |
| 28 | + self.warning( |
| 29 | + "in_scope_only is disabled. This module queries each IP individually and may consume a lot of API credits!" |
| 30 | + ) |
| 31 | + return True |
| 32 | + |
| 33 | + async def filter_event(self, event): |
| 34 | + in_scope_only = self.config.get("in_scope_only", True) |
| 35 | + max_scope_distance = 0 if in_scope_only else (self.scan.scope_search_distance + 1) |
| 36 | + if event.scope_distance > max_scope_distance: |
| 37 | + return False, "event is not in scope" |
25 | 38 | return True |
26 | 39 |
|
27 | 40 | async def handle_event(self, event): |
| 41 | + ip = event.data |
| 42 | + url = f"{self.base_url}/shodan/host/{self.helpers.quote(ip)}?key={{api_key}}" |
| 43 | + r = await self.api_request(url) |
| 44 | + if r is None: |
| 45 | + self.warning(f"No response from Shodan API for {ip}") |
| 46 | + return |
| 47 | + status_code = getattr(r, "status_code", 0) |
| 48 | + if status_code == 404: |
| 49 | + self.warning(f"No Shodan data about {ip}") |
| 50 | + return |
| 51 | + if not getattr(r, "is_success", False): |
| 52 | + self.warning(f"Shodan API error for {ip} (status {status_code})") |
| 53 | + return |
28 | 54 | try: |
29 | | - api = shodan.Shodan(self.api_key) |
30 | | - host = api.host(ips=event.data, history=False, minify=False) |
31 | | - |
32 | | - # ASN Extraction |
| 55 | + host = r.json() |
| 56 | + except Exception as e: |
| 57 | + self.warning(f"Failed to parse Shodan API response for {ip}: {e}") |
| 58 | + return |
| 59 | + |
| 60 | + # ASN Extraction |
| 61 | + asn_raw = host.get("asn", "") |
| 62 | + if asn_raw: |
33 | 63 | asn = { |
34 | | - "asn": host["asn"][2:], |
35 | | - "name": host["org"], |
36 | | - "description": host["isp"], |
37 | | - "country": host["country_code"], |
| 64 | + "asn": asn_raw[2:] if asn_raw.startswith("AS") else asn_raw, |
| 65 | + "name": host.get("org", ""), |
| 66 | + "description": host.get("isp", ""), |
| 67 | + "country": host.get("country_code", ""), |
38 | 68 | } |
39 | | - |
40 | 69 | await self.emit_event( |
41 | 70 | asn, |
42 | 71 | "ASN", |
43 | 72 | parent=event, |
44 | | - tags=host.get("tags"), |
45 | | - context=f"Shodan API {event.data} request and find ASN", |
| 73 | + tags=host.get("tags") or [], |
| 74 | + context=f"Shodan API {ip} request and find ASN", |
46 | 75 | ) |
47 | 76 |
|
48 | | - if "data" in host: |
49 | | - for data in host["data"]: |
50 | | - # TECHNOLOGY Extraction |
51 | | - ## TECHNOLOGY CPE Formats |
52 | | - if "cpe" in data: |
53 | | - for technology in data["cpe"]: |
54 | | - tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} |
55 | | - await self.emit_event( |
56 | | - tech, |
57 | | - "TECHNOLOGY", |
58 | | - parent=event, |
59 | | - tags=data.get("tags") or [], |
60 | | - context=f"Shodan API {event.data} request and find TECHNOLOGY: {technology}", |
61 | | - ) |
62 | | - |
63 | | - if "cpe23" in data: |
64 | | - for technology in data["cpe23"]: |
65 | | - tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} |
66 | | - await self.emit_event( |
67 | | - tech, |
68 | | - "TECHNOLOGY", |
69 | | - parent=event, |
70 | | - tags=data.get("tags") or [], |
71 | | - context=f"Shodan API {event.data} request and find TECHNOLOGY: {technology}", |
72 | | - ) |
73 | | - |
74 | | - # TECHNOLOGY Additional Formats |
75 | | - if "product" in data: |
76 | | - tech = { |
77 | | - "technology": data.get("product"), |
78 | | - "host": data.get("ip_str"), |
79 | | - "port": data.get("port"), |
80 | | - } |
81 | | - |
| 77 | + if "data" not in host: |
| 78 | + self.warning(f"No Shodan data about {ip}") |
| 79 | + return |
| 80 | + |
| 81 | + # NIST cvss score severity mapping |
| 82 | + severity_map = {"NONE": 0.0, "LOW": 0.1, "MEDIUM": 4.0, "HIGH": 7.0, "CRITICAL": 9.0} |
| 83 | + |
| 84 | + for data in host["data"]: |
| 85 | + # TECHNOLOGY Extraction |
| 86 | + ## TECHNOLOGY CPE Formats |
| 87 | + for technology in data.get("cpe", []): |
| 88 | + tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} |
| 89 | + await self.emit_event( |
| 90 | + tech, |
| 91 | + "TECHNOLOGY", |
| 92 | + parent=event, |
| 93 | + tags=data.get("tags") or [], |
| 94 | + context=f"Shodan API {ip} request and find TECHNOLOGY: {technology}", |
| 95 | + ) |
| 96 | + |
| 97 | + for technology in data.get("cpe23", []): |
| 98 | + tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} |
| 99 | + await self.emit_event( |
| 100 | + tech, |
| 101 | + "TECHNOLOGY", |
| 102 | + parent=event, |
| 103 | + tags=data.get("tags") or [], |
| 104 | + context=f"Shodan API {ip} request and find TECHNOLOGY: {technology}", |
| 105 | + ) |
| 106 | + |
| 107 | + # TECHNOLOGY Additional Formats |
| 108 | + if "product" in data: |
| 109 | + tech = { |
| 110 | + "technology": data.get("product"), |
| 111 | + "host": data.get("ip_str"), |
| 112 | + "port": data.get("port"), |
| 113 | + } |
| 114 | + await self.emit_event( |
| 115 | + tech, |
| 116 | + "TECHNOLOGY", |
| 117 | + parent=event, |
| 118 | + tags=data.get("tags") or [], |
| 119 | + context=f"Shodan API {ip} request and find TECHNOLOGY: {data['product']}", |
| 120 | + ) |
| 121 | + |
| 122 | + if "http" in data: |
| 123 | + if "components" in data["http"]: |
| 124 | + for technology in data["http"]["components"]: |
| 125 | + tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} |
| 126 | + tags = list(data["http"]["components"][technology].get("categories", [])) |
| 127 | + tags.append("web-technology") |
82 | 128 | await self.emit_event( |
83 | 129 | tech, |
84 | 130 | "TECHNOLOGY", |
85 | 131 | parent=event, |
86 | | - tags=data.get("tags") or [], |
87 | | - context=f"Shodan API {event.data} request and find TECHNOLOGY: {data['product']}", |
| 132 | + tags=tags, |
| 133 | + context=f"Shodan API {ip} request and find TECHNOLOGY: {technology}", |
88 | 134 | ) |
89 | 135 |
|
90 | | - if "http" in data: |
91 | | - if "components" in data["http"]: |
92 | | - for technology in data["http"]["components"]: |
93 | | - tech = {"technology": technology, "host": data.get("ip_str"), "port": data.get("port")} |
94 | | - tags = data["http"]["components"][technology]["categories"] |
95 | | - tags.append("web-technology") |
96 | | - await self.emit_event( |
97 | | - tech, |
98 | | - "TECHNOLOGY", |
99 | | - parent=event, |
100 | | - tags=tags or [], |
101 | | - context=f"Shodan API {event.data} request and find TECHNOLOGY: {technology}", |
102 | | - ) |
103 | | - |
104 | | - # OPEN_TCP_PORT, OPEN_UDP_PORT Extraction |
105 | | - if "port" in data and "transport" in data: |
106 | | - if data["transport"] == "tcp": |
107 | | - await self.emit_event( |
108 | | - self.helpers.make_netloc(event.data, data.get("port")), |
109 | | - "OPEN_TCP_PORT", |
110 | | - parent=event, |
111 | | - tags=data.get("tags") or [], |
112 | | - context=f"Shodan API {event.data} request and find TECHNOLOGY: {data.get('port')}", |
113 | | - ) |
114 | | - |
115 | | - elif data["transport"] == "udp": |
116 | | - await self.emit_event( |
117 | | - self.helpers.make_netloc(event.data, data.get("port")), |
118 | | - "OPEN_UDP_PORT", |
119 | | - parent=event, |
120 | | - tags=data.get("tags") or [], |
121 | | - context=f"Shodan API {event.data} request and find TECHNOLOGY: {data.get('port')}", |
122 | | - ) |
123 | | - |
124 | | - else: |
125 | | - self.warning(f"[WARNING] unknown transport {data['transport']}") |
126 | | - |
127 | | - # VULNERABILITY Extraction |
128 | | - # NIST cvss score severity mapping |
129 | | - severity_map = {"NONE": 0.0, "LOW": 0.1, "MEDIUM": 4.0, "HIGH": 7.0, "CRITICAL": 9.0} |
130 | | - |
131 | | - if "vulns" in data: |
132 | | - for item in data["vulns"]: |
133 | | - cve = item |
134 | | - vuln = { |
135 | | - "host": data.get("ip_str"), |
136 | | - "severity": max( |
137 | | - ( |
138 | | - level |
139 | | - for level, threshold in severity_map.items() |
140 | | - if data["vulns"][item].get("cvss") >= threshold |
141 | | - ), |
142 | | - key=lambda x: severity_map[x], |
143 | | - ), |
144 | | - "description": f"{cve}", |
145 | | - } |
146 | | - |
147 | | - await self.emit_event( |
148 | | - vuln, |
149 | | - "VULNERABILITY", |
150 | | - parent=event, |
151 | | - tags=[], |
152 | | - context=f"Shodan API {event.data} request and find VULNERABILITY {cve}", |
153 | | - ) |
154 | | - |
155 | | - else: |
156 | | - self.warning(f"No Shodan data about {event.data}") |
157 | | - |
158 | | - except shodan.APIError as e: |
159 | | - self.error(f"Shodan API error: {e}") |
| 136 | + # OPEN_TCP_PORT, OPEN_UDP_PORT Extraction |
| 137 | + if "port" in data and "transport" in data: |
| 138 | + if data["transport"] == "tcp": |
| 139 | + await self.emit_event( |
| 140 | + self.helpers.make_netloc(ip, data.get("port")), |
| 141 | + "OPEN_TCP_PORT", |
| 142 | + parent=event, |
| 143 | + tags=data.get("tags") or [], |
| 144 | + context=f"Shodan API {ip} request and find OPEN_TCP_PORT: {data.get('port')}", |
| 145 | + ) |
| 146 | + elif data["transport"] == "udp": |
| 147 | + await self.emit_event( |
| 148 | + self.helpers.make_netloc(ip, data.get("port")), |
| 149 | + "OPEN_UDP_PORT", |
| 150 | + parent=event, |
| 151 | + tags=data.get("tags") or [], |
| 152 | + context=f"Shodan API {ip} request and find OPEN_UDP_PORT: {data.get('port')}", |
| 153 | + ) |
| 154 | + else: |
| 155 | + self.warning(f"Unknown transport {data['transport']}") |
| 156 | + |
| 157 | + # VULNERABILITY Extraction |
| 158 | + if "vulns" in data: |
| 159 | + for cve, vuln_data in data["vulns"].items(): |
| 160 | + cvss = vuln_data.get("cvss", 0) |
| 161 | + severity = max( |
| 162 | + (level for level, threshold in severity_map.items() if cvss >= threshold), |
| 163 | + key=lambda x: severity_map[x], |
| 164 | + ) |
| 165 | + vuln = { |
| 166 | + "host": data.get("ip_str"), |
| 167 | + "severity": severity, |
| 168 | + "description": cve, |
| 169 | + } |
| 170 | + await self.emit_event( |
| 171 | + vuln, |
| 172 | + "VULNERABILITY", |
| 173 | + parent=event, |
| 174 | + tags=[], |
| 175 | + context=f"Shodan API {ip} request and find VULNERABILITY {cve}", |
| 176 | + ) |
0 commit comments