Skip to content

Commit db741fb

Browse files
authored
Merge branch 'waf-bypass' into better-text-compare
2 parents 323284e + bdce02d commit db741fb

File tree

5 files changed

+184
-182
lines changed

5 files changed

+184
-182
lines changed

bbot/core/event/helpers.py

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@
99
bbot_event_seeds = {}
1010

1111

12+
# Pre-compute sorted event classes for performance
13+
# This is computed once when the module is loaded instead of on every EventSeed() call
14+
def _get_sorted_event_classes():
15+
"""
16+
Sort event classes by priority (higher priority first).
17+
This ensures specific patterns like ASN:12345 are checked before broad patterns like hostname:port.
18+
"""
19+
return sorted(bbot_event_seeds.items(), key=lambda x: getattr(x[1], "priority", 5), reverse=True)
20+
21+
22+
# This will be populated after all event seed classes are registered
23+
_sorted_event_classes = None
24+
25+
1226
"""
1327
An "Event Seed" is a lightweight event containing only the minimum logic required to:
1428
- parse input to determine the event type + data
@@ -40,22 +54,25 @@ class EventSeedRegistry(type):
4054
"""
4155

4256
def __new__(mcs, name, bases, attrs):
43-
global bbot_event_seeds
57+
global bbot_event_seeds, _sorted_event_classes
4458
cls = super().__new__(mcs, name, bases, attrs)
4559
# Don't register the base EventSeed class
4660
if name != "BaseEventSeed":
4761
bbot_event_seeds[cls.__name__] = cls
62+
# Recompute sorted classes whenever a new event seed is registered
63+
_sorted_event_classes = _get_sorted_event_classes()
4864
return cls
4965

5066

5167
def EventSeed(input):
5268
input = smart_encode_punycode(smart_decode(input).strip())
5369

54-
# Sort event classes by priority (higher priority first)
55-
# This ensures specific patterns like ASN:12345 are checked before broad patterns like hostname:port
56-
sorted_event_classes = sorted(bbot_event_seeds.items(), key=lambda x: getattr(x[1], "priority", 5), reverse=True)
70+
# Use pre-computed sorted event classes for better performance
71+
global _sorted_event_classes
72+
if _sorted_event_classes is None:
73+
_sorted_event_classes = _get_sorted_event_classes()
5774

58-
for _, event_class in sorted_event_classes:
75+
for _, event_class in _sorted_event_classes:
5976
if hasattr(event_class, "precheck"):
6077
if event_class.precheck(input):
6178
return event_class(input)
@@ -272,16 +289,15 @@ def _override_input(self, input):
272289
# This method resolves the ASN to a list of IP_RANGES using the ASN API, and then adds the cidr string as a child event seed.
273290
# These will later be automatically resolved to an IP_RANGE event seed and added to the target.
274291
async def _generate_children(self, helpers):
275-
asns = await helpers.asn.asn_to_subnets(int(self.data))
292+
asn_data = await helpers.asn.asn_to_subnets(int(self.data))
276293
children = []
277-
if asns:
278-
for asn in asns:
279-
subnets = asn.get("subnets")
280-
if isinstance(subnets, str):
281-
subnets = [subnets]
282-
if subnets:
283-
for cidr in subnets:
284-
children.append(cidr)
294+
if asn_data:
295+
subnets = asn_data.get("subnets")
296+
if isinstance(subnets, str):
297+
subnets = [subnets]
298+
if subnets:
299+
for cidr in subnets:
300+
children.append(cidr)
285301
return children
286302

287303
@staticmethod

bbot/core/helpers/asn.py

Lines changed: 103 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import logging
33
import asyncio
44
from radixtarget.tree.ip import IPRadixTree
5+
from cachetools import LRUCache
56

67
log = logging.getLogger("bbot.core.helpers.asn")
78

@@ -15,45 +16,86 @@ def __init__(self, parent_helper):
1516
# IP radix trees (authoritative store) – IPv4 and IPv6
1617
self._tree4: IPRadixTree = IPRadixTree()
1718
self._tree6: IPRadixTree = IPRadixTree()
18-
self._subnet_to_asn_cache: dict[str, list] = {}
19+
# LRU caches with reasonable limits to prevent unbounded memory growth
20+
self._subnet_to_asn_cache: LRUCache = LRUCache(maxsize=10000) # Cache subnet -> ASN mappings
1921
# ASN cache (ASN ID -> data mapping)
20-
self._asn_to_data_cache: dict[int, list] = {}
22+
self._asn_to_data_cache: LRUCache = LRUCache(maxsize=5000) # Cache ASN records
2123

2224
# Default record used when no ASN data can be found
2325
UNKNOWN_ASN = {
2426
"asn": "0",
2527
"subnets": [],
2628
"name": "unknown",
2729
"description": "unknown",
28-
"country": "",
30+
"country": "unknown",
2931
}
3032

3133
async def _request_with_retry(self, url, max_retries=10):
34+
log.critical(f"ASN API request: {url}")
3235
"""Make request with retry for 429 responses using Retry-After header."""
3336
for attempt in range(max_retries + 1):
3437
response = await self.parent_helper.request(url, timeout=15)
35-
if response is None or getattr(response, "status_code", 0) != 429:
38+
if response is None or getattr(response, "status_code", 0) == 200:
3639
log.debug(f"ASN API request successful, status code: {getattr(response, 'status_code', 0)}")
3740
return response
3841

39-
if attempt < max_retries:
40-
log.debug(f"ASN API rate limited, attempt {attempt + 1}")
41-
# Get retry-after header value, default to 1 second if not present
42-
retry_after = getattr(response, "headers", {}).get("retry-after", "1")
43-
try:
44-
delay = int(retry_after) + 1
45-
except (ValueError, TypeError):
46-
delay = 2 # fallback if header is invalid
47-
48-
log.debug(
49-
f"ASN API rate limited, waiting {delay}s (retry-after: {retry_after}) (attempt {attempt + 1})"
50-
)
51-
await asyncio.sleep(delay)
42+
elif getattr(response, "status_code", 0) == 429:
43+
if attempt < max_retries:
44+
attempt += 1
45+
# Get retry-after header value, default to 1 second if not present
46+
retry_after = getattr(response, "headers", {}).get("retry-after", "10")
47+
delay = int(retry_after)
48+
log.verbose(
49+
f"ASN API rate limited, waiting {delay}s (retry-after: {retry_after}) (attempt {attempt})"
50+
)
51+
await asyncio.sleep(delay)
52+
else:
53+
log.warning(f"ASN API gave up after {max_retries + 1} attempts due to repeatedrate limiting")
54+
elif getattr(response, "status_code", 0) == 404:
55+
log.debug(f"ASN API returned 404 for {url}")
56+
return None
5257
else:
53-
log.warning(f"ASN API gave up after {max_retries + 1} attempts due to rate limiting")
58+
log.warning(
59+
f"Got unexpected status code: {getattr(response, 'status_code', 0)} from ASN DB api ({url})"
60+
)
61+
return None
5462

5563
return response
5664

65+
async def _query_api(self, identifier, url_base, processor_method):
66+
"""Common API query method that handles request/response pattern."""
67+
url = f"{url_base}{identifier}"
68+
response = await self._request_with_retry(url)
69+
if response is None:
70+
log.warning(f"ASN DB API: no response for {identifier}")
71+
return None
72+
73+
status = getattr(response, "status_code", 0)
74+
if status != 200:
75+
return None
76+
77+
try:
78+
raw = response.json()
79+
except Exception as e:
80+
log.warning(f"ASN DB API: JSON decode error for {identifier}: {e}")
81+
return None
82+
83+
if isinstance(raw, dict):
84+
return processor_method(raw, identifier)
85+
86+
log.warning(f"ASN DB API: returned unexpected format for {identifier}: {raw}")
87+
return None
88+
89+
def _build_asn_record(self, raw, subnets):
90+
"""Build standardized ASN record from API response."""
91+
return {
92+
"asn": str(raw.get("asn", "")),
93+
"subnets": subnets,
94+
"name": raw.get("asn_name") or "",
95+
"description": raw.get("org") or "",
96+
"country": raw.get("country") or "",
97+
}
98+
5799
async def asn_to_subnets(self, asn):
58100
"""Return subnets for *asn* using cached subnet ranges where possible."""
59101
# Handle both int and str inputs
@@ -64,7 +106,7 @@ async def asn_to_subnets(self, asn):
64106
asn_int = int(str(asn.lower()).lstrip("as"))
65107
except ValueError:
66108
log.warning(f"Invalid ASN format: {asn}")
67-
return [self.UNKNOWN_ASN]
109+
return self.UNKNOWN_ASN
68110

69111
cached = self._cache_lookup_asn(asn_int)
70112
if cached is not None:
@@ -76,7 +118,7 @@ async def asn_to_subnets(self, asn):
76118
if asn_data:
77119
self._cache_store_asn(asn_data, asn_int)
78120
return asn_data
79-
return [self.UNKNOWN_ASN]
121+
return self.UNKNOWN_ASN
80122

81123
async def ip_to_subnets(self, ip: str):
82124
"""Return ASN info for *ip* using cached subnet ranges where possible."""
@@ -85,115 +127,65 @@ async def ip_to_subnets(self, ip: str):
85127
cached = self._cache_lookup_ip(ip_str)
86128
if cached is not None:
87129
log.debug(f"cache HIT for ip: {ip_str}")
88-
return cached or [self.UNKNOWN_ASN]
130+
return cached or self.UNKNOWN_ASN
89131

90132
log.debug(f"cache MISS for ip: {ip_str}")
91133
asn_data = await self._query_api_ip(ip_str)
92134
if asn_data:
93135
self._cache_store_ip(asn_data)
94136
return asn_data
95-
return [self.UNKNOWN_ASN]
137+
return self.UNKNOWN_ASN
96138

97139
async def _query_api_ip(self, ip: str):
98-
# Build request URL using overridable base
99-
url = f"{self.asndb_ip_url}{ip}"
100-
response = await self._request_with_retry(url)
101-
if response is None:
102-
log.warning(f"ASN DB API: no response for {ip}")
103-
return None
104-
105-
status = getattr(response, "status_code", 0)
106-
if status != 200:
107-
log.warning(f"ASN DB API: returned {status} for {ip}")
108-
return None
109-
110-
try:
111-
raw = response.json()
112-
except Exception as e:
113-
log.warning(f"ASN DB API: JSON decode error for {ip}: {e}")
114-
return None
115-
116-
if isinstance(raw, dict):
117-
subnets = raw.get("subnets")
118-
if isinstance(subnets, str):
119-
subnets = [subnets]
120-
if not subnets:
121-
subnets = [f"{ip}/32"]
122-
123-
rec = {
124-
"asn": str(raw.get("asn", "")),
125-
"subnets": subnets,
126-
"name": raw.get("asn_name", ""),
127-
"description": raw.get("org", ""),
128-
"country": raw.get("country", ""),
129-
}
130-
return [rec]
131-
132-
log.warning(f"ASN DB API: returned unexpected format for {ip}: {raw}")
133-
return None
140+
"""Query ASN DB API for IP address information."""
141+
return await self._query_api(ip, self.asndb_ip_url, self._process_ip_response)
142+
143+
def _process_ip_response(self, raw, ip):
144+
"""Process IP lookup response from ASN DB API."""
145+
subnets = raw.get("subnets", [])
146+
# API returns subnets as array, but handle string case for safety
147+
if isinstance(subnets, str):
148+
subnets = [subnets]
149+
if not subnets:
150+
subnets = [f"{ip}/32"]
151+
return self._build_asn_record(raw, subnets)
134152

135153
async def _query_api_asn(self, asn: str):
136-
url = f"{self.asndb_asn_url}{asn}"
137-
response = await self._request_with_retry(url)
138-
if response is None:
139-
log.warning(f"ASN DB API: no response for {asn}")
140-
return None
141-
142-
status = getattr(response, "status_code", 0)
143-
if status != 200:
144-
log.warning(f"ASN DB API: returned {status} for {asn}")
145-
return None
146-
147-
try:
148-
raw = response.json()
149-
except Exception as e:
150-
log.warning(f"ASN DB API: JSON decode error for {asn}: {e}")
151-
return None
152-
153-
if isinstance(raw, dict):
154-
subnets = raw.get("subnets")
155-
if isinstance(subnets, str):
156-
subnets = [subnets]
157-
if not subnets:
158-
subnets = []
159-
160-
rec = {
161-
"asn": str(raw.get("asn", "")),
162-
"subnets": subnets,
163-
"name": raw.get("asn_name", ""),
164-
"description": raw.get("org", ""),
165-
"country": raw.get("country", ""),
166-
}
167-
return [rec]
168-
169-
log.warning(f"ASN DB API: returned unexpected format for {asn}: {raw}")
170-
return None
171-
172-
def _cache_store_asn(self, asn_list, asn_id: int):
154+
"""Query ASN DB API for ASN information."""
155+
return await self._query_api(asn, self.asndb_asn_url, self._process_asn_response)
156+
157+
def _process_asn_response(self, raw, asn):
158+
"""Process ASN lookup response from ASN DB API."""
159+
subnets = raw.get("subnets", [])
160+
# API returns subnets as array, but handle string case for safety
161+
if isinstance(subnets, str):
162+
subnets = [subnets]
163+
return self._build_asn_record(raw, subnets)
164+
165+
def _cache_store_asn(self, asn_record, asn_id: int):
173166
"""Cache ASN data by ASN ID"""
174-
self._asn_to_data_cache[asn_id] = asn_list
175-
log.debug(f"ASN cache ADD {asn_id} -> {asn_list[0].get('asn', '?') if asn_list else '?'}")
167+
self._asn_to_data_cache[asn_id] = asn_record
168+
log.debug(f"ASN cache ADD {asn_id} -> {asn_record.get('asn', '?') if asn_record else '?'}")
176169

177170
def _cache_lookup_asn(self, asn_id: int):
178171
"""Lookup cached ASN data by ASN ID"""
179172
return self._asn_to_data_cache.get(asn_id)
180173

181-
def _cache_store_ip(self, asn_list):
174+
def _cache_store_ip(self, asn_record):
182175
if not (self._tree4 or self._tree6):
183176
return
184-
for rec in asn_list:
185-
subnets = rec.get("subnets") or []
186-
if isinstance(subnets, str):
187-
subnets = [subnets]
188-
for p in subnets:
189-
try:
190-
net = ipaddress.ip_network(p, strict=False)
191-
except ValueError:
192-
continue
193-
tree = self._tree4 if net.version == 4 else self._tree6
194-
tree.insert(str(net), data=asn_list)
195-
self._subnet_to_asn_cache[str(net)] = asn_list
196-
log.debug(f"IP cache ADD {net} -> {asn_list[:1][0].get('asn', '?')}")
177+
subnets = asn_record.get("subnets") or []
178+
if isinstance(subnets, str):
179+
subnets = [subnets]
180+
for p in subnets:
181+
try:
182+
net = ipaddress.ip_network(p, strict=False)
183+
except ValueError:
184+
continue
185+
tree = self._tree4 if net.version == 4 else self._tree6
186+
tree.insert(str(net), data=asn_record)
187+
self._subnet_to_asn_cache[str(net)] = asn_record
188+
log.debug(f"IP cache ADD {net} -> {asn_record.get('asn', '?')}")
197189

198190
def _cache_lookup_ip(self, ip: str):
199191
ip_obj = ipaddress.ip_address(ip)

bbot/modules/report/asn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ async def handle_event(self, event):
5858

5959
asn_data = await self.helpers.asn.ip_to_subnets(host_str)
6060
if asn_data:
61-
asn_record = asn_data[0]
61+
asn_record = asn_data
6262
asn_number = asn_record.get("asn")
6363
asn_desc = asn_record.get("description", "")
6464
asn_name = asn_record.get("name", "")

0 commit comments

Comments
 (0)