Skip to content

Commit ed020b8

Browse files
Merge pull request #2413 from blacklanternsecurity/fix-shodan-ratelimit
Fix shodan internetdb ratelimit
2 parents 8f8061f + ea98f26 commit ed020b8

4 files changed

Lines changed: 44 additions & 10 deletions

File tree

bbot/defaults.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ web:
9292
# These are attached to all in-scope HTTP requests
9393
# Note that some modules (e.g. github) may end up sending these to out-of-scope resources
9494
http_headers: {}
95-
# HTTP retries (for Python requests; API calls, etc.)
95+
# How many times to retry API requests
96+
# Note that this is a separate mechanism on top of HTTP retries
97+
# which will retry API requests that don't return a successful status code
98+
api_retries: 2
99+
# HTTP retries - try again if the raw connection fails
96100
http_retries: 1
97101
# HTTP retries (for httpx)
98102
httpx_retries: 1

bbot/modules/base.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@ class BaseModule:
104104
_batch_size = 1
105105
batch_wait = 10
106106

107-
# API retries, etc.
108-
_api_retries = 2
109107
# disable the module after this many failed attempts in a row
110108
_api_failure_abort_threshold = 3
111109

@@ -157,6 +155,8 @@ def __init__(self, scan):
157155
# track number of failures (for .api_request())
158156
self._api_request_failures = 0
159157

158+
self._default_api_retries = self.scan.config.get("web", {}).get("api_retries", 2)
159+
160160
self._tasks = []
161161
self._event_received = None
162162

@@ -340,7 +340,7 @@ def cycle_api_key(self):
340340

341341
@property
342342
def api_retries(self):
343-
return max(self._api_retries + 1, len(self._api_keys))
343+
return max(self._default_api_retries + 1, len(self._api_keys))
344344

345345
@property
346346
def api_failure_abort_threshold(self):
@@ -1212,7 +1212,8 @@ def _prepare_api_iter_req(self, url, page, page_size, offset, **requests_kwargs)
12121212
return url, requests_kwargs
12131213

12141214
def _api_response_is_success(self, r):
1215-
return r.is_success
1215+
# 404s typically indicate no data rather than an actual error with the API, so we don't want to retry them
1216+
return getattr(r, "is_success", False) or getattr(r, "status_code", 0) == 404
12161217

12171218
async def api_page_iter(self, url, page_size=100, _json=True, next_key=None, iter_key=None, **requests_kwargs):
12181219
"""

bbot/modules/shodan_idb.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from bbot.modules.base import BaseModule
2+
import time
23

34

45
class shodan_idb(BaseModule):
@@ -46,23 +47,48 @@ class shodan_idb(BaseModule):
4647
"created_date": "2023-12-22",
4748
"author": "@TheTechromancer",
4849
}
50+
options = {"retries": None}
51+
options_desc = {
52+
"retries": "How many times to retry API requests (e.g. after a 429 error). Overrides the global web.api_retries setting."
53+
}
4954

50-
# we get lots of 404s, that's normal
55+
# we typically don't want to abort this module
5156
_api_failure_abort_threshold = 9999999999
5257

53-
# there aren't any rate limits to speak of, so our outgoing queue can be pretty big
54-
_qsize = 500
58+
# since there are rate limits, we set a lower qsize
59+
# this way when our queue is full, we can give the API a break
60+
_qsize = 100
5561

5662
base_url = "https://internetdb.shodan.io"
5763

64+
async def setup(self):
65+
await super().setup()
66+
self.last_request_time = 0
67+
return True
68+
5869
def _incoming_dedup_hash(self, event):
5970
return hash(self.get_ip(event))
6071

72+
@property
73+
def api_retries(self):
74+
# allow the module to override global retry setting
75+
return self.config.get("retries", None) or super().api_retries
76+
6177
async def handle_event(self, event):
6278
ip = self.get_ip(event)
6379
if ip is None:
6480
return
6581
url = f"{self.base_url}/{ip}"
82+
83+
# Rate limiting: ensure at least 1 second between requests
84+
current_time = time.time()
85+
time_since_last = current_time - self.last_request_time
86+
if time_since_last < 1:
87+
await self.helpers.sleep(1 - time_since_last)
88+
89+
# Update the last request time
90+
self.last_request_time = time.time()
91+
6692
r = await self.api_request(url)
6793
if r is None:
6894
self.debug(f"No response for {event.data}")

bbot/modules/templates/webhook.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,9 @@ class WebhookOutputModule(BaseOutputModule):
1616
# abort module after 10 failed requests (not including retries)
1717
_api_failure_abort_threshold = 10
1818
# retry each request up to 10 times, respecting the Retry-After header
19-
_api_retries = 10
19+
_default_api_retries = 10
2020

2121
async def setup(self):
22-
self._api_retries = self.config.get("retries", 10)
2322
self.webhook_url = self.config.get("webhook_url", "")
2423
self.min_severity = self.config.get("min_severity", "LOW").strip().upper()
2524
assert self.min_severity in self.vuln_severities, (
@@ -31,6 +30,10 @@ async def setup(self):
3130
return False
3231
return await super().setup()
3332

33+
@property
34+
def api_retries(self):
35+
return self.config.get("retries", self._default_api_retries)
36+
3437
async def handle_event(self, event):
3538
message = self.format_message(event)
3639
data = {self.content_key: message}

0 commit comments

Comments
 (0)