@osqd/bothandlerjs 0.5.0 → 0.6.0
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.
- package/dist/adapters/index.cjs +3 -0
- package/dist/adapters/index.cjs.map +1 -1
- package/dist/adapters/index.js +3 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/cli.cjs +459 -6
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +459 -6
- package/dist/cli.js.map +1 -1
- package/dist/core.d.ts +15 -0
- package/dist/corpus/index.cjs +84 -0
- package/dist/corpus/index.cjs.map +1 -1
- package/dist/corpus/index.js +84 -0
- package/dist/corpus/index.js.map +1 -1
- package/dist/corpus/schema.d.ts +8 -0
- package/dist/crawler-ranges.d.ts +31 -0
- package/dist/detectors/id-enumeration.d.ts +31 -0
- package/dist/detectors/index.d.ts +8 -0
- package/dist/detectors/known-bots.d.ts +11 -0
- package/dist/detectors/parameter-sweep.d.ts +39 -0
- package/dist/detectors/probe-volume.d.ts +26 -0
- package/dist/detectors/transport-coherence.d.ts +31 -0
- package/dist/element/index.cjs +4 -1
- package/dist/element/index.cjs.map +1 -1
- package/dist/element/index.js +4 -1
- package/dist/element/index.js.map +1 -1
- package/dist/index.cjs +401 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +396 -12
- package/dist/index.js.map +1 -1
- package/dist/state.d.ts +75 -0
- package/dist/types.d.ts +36 -0
- package/docs/course/05-detectors.md +4 -0
- package/docs/detection/detectors.md +122 -0
- package/docs/detection/signatures.md +9 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -144,16 +144,20 @@ function hashString(value) {
|
|
|
144
144
|
}
|
|
145
145
|
return hash >>> 0;
|
|
146
146
|
}
|
|
147
|
-
var TIMESTAMP_RING, PATH_CAP, UA_CAP, MAX_TRACKED_ARRIVALS, MAX_TRACKED_PATHS, MAX_TRACKED_USER_AGENTS, ActorState, ActorRegistry;
|
|
147
|
+
var TIMESTAMP_RING, PATH_CAP, QUERY_CAP, METHOD_CAP, WALK_CAP, UA_CAP, MAX_TRACKED_ARRIVALS, MAX_TRACKED_PATHS, MAX_TRACKED_QUERIES, MAX_TRACKED_USER_AGENTS, ActorState, ActorRegistry;
|
|
148
148
|
var init_state = __esm({
|
|
149
149
|
"src/state.ts"() {
|
|
150
150
|
"use strict";
|
|
151
151
|
init_lru();
|
|
152
152
|
TIMESTAMP_RING = 32;
|
|
153
153
|
PATH_CAP = 64;
|
|
154
|
+
QUERY_CAP = 64;
|
|
155
|
+
METHOD_CAP = 12;
|
|
156
|
+
WALK_CAP = 4;
|
|
154
157
|
UA_CAP = 4;
|
|
155
158
|
MAX_TRACKED_ARRIVALS = TIMESTAMP_RING;
|
|
156
159
|
MAX_TRACKED_PATHS = PATH_CAP;
|
|
160
|
+
MAX_TRACKED_QUERIES = QUERY_CAP;
|
|
157
161
|
MAX_TRACKED_USER_AGENTS = UA_CAP;
|
|
158
162
|
ActorState = class {
|
|
159
163
|
key;
|
|
@@ -195,6 +199,45 @@ var init_state = __esm({
|
|
|
195
199
|
paths = /* @__PURE__ */ new Set();
|
|
196
200
|
pathsOverflowed = false;
|
|
197
201
|
pathsSaturatedAtTotal = 0;
|
|
202
|
+
/**
|
|
203
|
+
* Distinct *parameterised* requests: the path together with its query.
|
|
204
|
+
*
|
|
205
|
+
* Counted apart from `paths` because the two answer different questions and a scraper
|
|
206
|
+
* lives in the gap between them. `/products?page=1` through `?page=200` is one path and
|
|
207
|
+
* two hundred requests, so breadth reads it as somebody rereading a single page — which
|
|
208
|
+
* is exactly what enumerating a catalogue looks like from the path alone.
|
|
209
|
+
*/
|
|
210
|
+
queries = /* @__PURE__ */ new Set();
|
|
211
|
+
queriesOverflowed = false;
|
|
212
|
+
/**
|
|
213
|
+
* Which HTTP methods this actor has used.
|
|
214
|
+
*
|
|
215
|
+
* A browser navigating issues GET. Something that has issued nothing but HEAD across a
|
|
216
|
+
* long visit is checking what exists rather than reading it, and that is a fact about
|
|
217
|
+
* the actor rather than about any one of its requests — which is why it is kept here.
|
|
218
|
+
*/
|
|
219
|
+
methods = /* @__PURE__ */ new Set();
|
|
220
|
+
/**
|
|
221
|
+
* Numeric walks in progress, by path shape: `/user/#` against the ids requested under it.
|
|
222
|
+
*
|
|
223
|
+
* Three numbers per shape, deliberately — a count, a lowest and a highest — rather than
|
|
224
|
+
* the ids themselves. What separates enumeration from reading is not which ids were
|
|
225
|
+
* asked for but whether they *cover a range*: thirty requests spanning thirty
|
|
226
|
+
* consecutive ids is a walk, and thirty scattered across a hundred thousand is somebody
|
|
227
|
+
* following links. Both are answerable from a count and a span, and only the count and
|
|
228
|
+
* the span survive an actor asking for ten thousand of them.
|
|
229
|
+
*/
|
|
230
|
+
walks = /* @__PURE__ */ new Map();
|
|
231
|
+
/**
|
|
232
|
+
* What the application answered, for the requests anybody bothered to tell us about.
|
|
233
|
+
*
|
|
234
|
+
* The engine decides *before* the response exists, so this arrives afterwards and only
|
|
235
|
+
* when the adapter reports it. Kept as two counters rather than a list because the one
|
|
236
|
+
* question worth asking is a ratio: an actor whose requests are almost all misses is
|
|
237
|
+
* looking for something rather than reading anything.
|
|
238
|
+
*/
|
|
239
|
+
responsesSeen = 0;
|
|
240
|
+
missesSeen = 0;
|
|
198
241
|
userAgents = /* @__PURE__ */ new Set();
|
|
199
242
|
constructor(key, now) {
|
|
200
243
|
this.key = key;
|
|
@@ -214,6 +257,15 @@ var init_state = __esm({
|
|
|
214
257
|
this.pathsOverflowed = true;
|
|
215
258
|
this.pathsSaturatedAtTotal = this.total;
|
|
216
259
|
}
|
|
260
|
+
const keys = Object.keys(facts.query).sort();
|
|
261
|
+
if (keys.length > 0) {
|
|
262
|
+
const signature = `${facts.path}?${keys.map((key) => `${key}=${facts.query[key] ?? ""}`).join("&")}`;
|
|
263
|
+
const queryHash = hashString(signature);
|
|
264
|
+
if (this.queries.size < QUERY_CAP) this.queries.add(queryHash);
|
|
265
|
+
else if (!this.queries.has(queryHash)) this.queriesOverflowed = true;
|
|
266
|
+
}
|
|
267
|
+
if (this.methods.size < METHOD_CAP) this.methods.add(facts.method);
|
|
268
|
+
this.noteWalk(facts.path);
|
|
217
269
|
const ua = facts.headers["user-agent"];
|
|
218
270
|
if (ua !== void 0 && this.userAgents.size < UA_CAP) this.userAgents.add(ua);
|
|
219
271
|
}
|
|
@@ -224,6 +276,79 @@ var init_state = __esm({
|
|
|
224
276
|
get pathsSaturated() {
|
|
225
277
|
return this.pathsOverflowed;
|
|
226
278
|
}
|
|
279
|
+
/** Distinct path-and-query combinations seen. Saturates at {@link QUERY_CAP}. */
|
|
280
|
+
get distinctQueries() {
|
|
281
|
+
return this.queries.size;
|
|
282
|
+
}
|
|
283
|
+
get queriesSaturated() {
|
|
284
|
+
return this.queriesOverflowed;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Records what the application answered. Called after the response, if at all.
|
|
288
|
+
*
|
|
289
|
+
* 404 and 410 only. A 403 is usually this library's own doing and counting it would
|
|
290
|
+
* make the detector that reads this argue with itself; a 500 is the site's problem and
|
|
291
|
+
* says nothing about the client.
|
|
292
|
+
*/
|
|
293
|
+
recordOutcome(status) {
|
|
294
|
+
this.responsesSeen++;
|
|
295
|
+
if (status === 404 || status === 410) this.missesSeen++;
|
|
296
|
+
}
|
|
297
|
+
/** Responses reported for this actor. Zero unless something is reporting them. */
|
|
298
|
+
get responses() {
|
|
299
|
+
return this.responsesSeen;
|
|
300
|
+
}
|
|
301
|
+
/** Of those, how many were 404 or 410. */
|
|
302
|
+
get misses() {
|
|
303
|
+
return this.missesSeen;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Files a request under the shape of its path, if that path carries a number.
|
|
307
|
+
*
|
|
308
|
+
* The last numeric segment is the one taken to be the identifier: in `/api/v2/orders/42`
|
|
309
|
+
* the version is part of the shape and the order id is what is being walked.
|
|
310
|
+
*/
|
|
311
|
+
noteWalk(path) {
|
|
312
|
+
const segments = path.split("/");
|
|
313
|
+
let value;
|
|
314
|
+
let template = "";
|
|
315
|
+
for (const segment of segments) {
|
|
316
|
+
if (segment !== "" && /^\d+$/.test(segment)) {
|
|
317
|
+
const parsed = Number(segment);
|
|
318
|
+
if (Number.isSafeInteger(parsed) && parsed <= 1e7) value = parsed;
|
|
319
|
+
template += "/#";
|
|
320
|
+
} else if (segment !== "") {
|
|
321
|
+
template += `/${segment}`;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (value === void 0) return;
|
|
325
|
+
const existing = this.walks.get(template);
|
|
326
|
+
if (existing !== void 0) {
|
|
327
|
+
existing.count++;
|
|
328
|
+
if (value < existing.min) existing.min = value;
|
|
329
|
+
if (value > existing.max) existing.max = value;
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
if (this.walks.size < WALK_CAP) this.walks.set(template, { count: 1, min: value, max: value });
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* The path shape this actor has walked hardest, with how far it reached.
|
|
336
|
+
*
|
|
337
|
+
* `span` is inclusive of both ends, so a walk of 1 to 30 spans 30. Comparing the count
|
|
338
|
+
* against it is what separates covering a range from visiting a few points in one.
|
|
339
|
+
*/
|
|
340
|
+
densestWalk() {
|
|
341
|
+
let best;
|
|
342
|
+
for (const [template, walk] of this.walks) {
|
|
343
|
+
const span = walk.max - walk.min + 1;
|
|
344
|
+
if (best === void 0 || walk.count > best.count) best = { template, count: walk.count, span };
|
|
345
|
+
}
|
|
346
|
+
return best;
|
|
347
|
+
}
|
|
348
|
+
/** Every HTTP method this actor has used, in first-seen order. */
|
|
349
|
+
get methodsSeen() {
|
|
350
|
+
return [...this.methods];
|
|
351
|
+
}
|
|
227
352
|
/**
|
|
228
353
|
* Requests seen when {@link distinctPaths} stopped being able to grow, or 0 if it
|
|
229
354
|
* still can. Over that many requests the distinct count is exact, so it is the only
|
|
@@ -312,6 +437,12 @@ var init_state = __esm({
|
|
|
312
437
|
key: this.key,
|
|
313
438
|
requests: this.total,
|
|
314
439
|
distinctPaths: this.distinctPaths,
|
|
440
|
+
distinctQueries: this.distinctQueries,
|
|
441
|
+
methodsSeen: this.methodsSeen,
|
|
442
|
+
walk: this.densestWalk(),
|
|
443
|
+
responses: this.responses,
|
|
444
|
+
misses: this.misses,
|
|
445
|
+
queriesSaturated: this.queriesSaturated,
|
|
315
446
|
firstSeen: this.firstSeen,
|
|
316
447
|
lastSeen: this.lastSeen,
|
|
317
448
|
sinceLastMs: this.sinceLast(now),
|
|
@@ -1905,7 +2036,7 @@ function compileSignatures(signatures = BOT_SIGNATURES) {
|
|
|
1905
2036
|
function indexSignatures(signatures = BOT_SIGNATURES) {
|
|
1906
2037
|
return new Map(signatures.map((signature) => [signature.id, signature]));
|
|
1907
2038
|
}
|
|
1908
|
-
var BOT_CATEGORIES, SEARCH, AI, SEO, SOCIAL, MONITORING, ARCHIVE, FEED, SECURITY, LIBRARY, HEADLESS, EMBEDDED, ADVERTISING, COMMERCE, ACADEMIC, ACCESSIBILITY, BOT_SIGNATURES, BENIGN_CATEGORIES;
|
|
2039
|
+
var BOT_CATEGORIES, SEARCH, AI, SEO, SOCIAL, MONITORING, ARCHIVE, FEED, SECURITY, LIBRARY, HEADLESS, EMBEDDED, ADVERTISING, COMMERCE, ACADEMIC, EMAIL_SECURITY, ACCESSIBILITY, BOT_SIGNATURES, BENIGN_CATEGORIES;
|
|
1909
2040
|
var init_known_bots = __esm({
|
|
1910
2041
|
"src/detectors/known-bots.ts"() {
|
|
1911
2042
|
"use strict";
|
|
@@ -1926,6 +2057,7 @@ var init_known_bots = __esm({
|
|
|
1926
2057
|
"commerce",
|
|
1927
2058
|
"accessibility",
|
|
1928
2059
|
"academic",
|
|
2060
|
+
"email-security",
|
|
1929
2061
|
"other"
|
|
1930
2062
|
];
|
|
1931
2063
|
SEARCH = [
|
|
@@ -1950,6 +2082,13 @@ var init_known_bots = __esm({
|
|
|
1950
2082
|
{ id: "yisouspider", name: "Shenma (Yisou) Spider", tokens: ["yisouspider"], category: "search", benign: true, robotsAgent: "YisouSpider", verification: { kind: "none" } },
|
|
1951
2083
|
{ id: "yahoo-slurp", name: "Yahoo! Slurp", tokens: ["yahoo! slurp"], category: "search", benign: true, robotsAgent: "Slurp", verification: { kind: "fcrdns", domains: ["crawl.yahoo.net", "yahoo.com"] } },
|
|
1952
2084
|
{ id: "mail-ru", name: "Mail.Ru bot", tokens: ["mail.ru_bot"], category: "search", benign: true, verification: { kind: "none" } },
|
|
2085
|
+
{ id: "brave-search", name: "Brave Search", tokens: ["bravesearchbot"], category: "search", benign: true, robotsAgent: "BraveSearchBot", verification: { kind: "none" } },
|
|
2086
|
+
{ id: "ecosia", name: "Ecosia", tokens: ["ecosiabot"], category: "search", benign: true, verification: { kind: "none" } },
|
|
2087
|
+
{ id: "startpage", name: "Startpage", tokens: ["startpagebot"], category: "search", benign: true, verification: { kind: "none" } },
|
|
2088
|
+
{ id: "daum", name: "Daumoa", tokens: ["daumoa"], category: "search", benign: true, verification: { kind: "none" } },
|
|
2089
|
+
{ id: "stract", name: "Stract", tokens: ["stractbot"], category: "search", benign: true, verification: { kind: "none" } },
|
|
2090
|
+
{ id: "rightdao", name: "RightDao", tokens: ["rightdaobot"], category: "search", benign: true, verification: { kind: "none" } },
|
|
2091
|
+
{ id: "gigablast", name: "Gigablast", tokens: ["gigablastopensource"], category: "search", benign: true, verification: { kind: "none" } },
|
|
1953
2092
|
{ id: "exabot", name: "Exabot (Exalead)", tokens: ["exabot"], category: "search", benign: true, verification: { kind: "none" } },
|
|
1954
2093
|
{ id: "kagibot", name: "Kagi", tokens: ["kagibot"], category: "search", benign: true, robotsAgent: "KagiBot", verification: { kind: "none" } }
|
|
1955
2094
|
];
|
|
@@ -2020,11 +2159,15 @@ var init_known_bots = __esm({
|
|
|
2020
2159
|
{ id: "pinterestbot", name: "Pinterestbot", tokens: ["pinterest/", "pinterestbot"], category: "social", benign: true, verification: { kind: "fcrdns", domains: ["pinterest.com"] } },
|
|
2021
2160
|
{ id: "mastodon", name: "Mastodon / Fediverse", tokens: ["mastodon/", "pleroma", "misskey/", "akkoma"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2022
2161
|
{ id: "embedly", name: "Embedly", tokens: ["embedly"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2023
|
-
{ id: "bluesky", name: "Bluesky card fetcher", tokens: ["bluesky cardyb", "cardyb/"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2162
|
+
{ id: "bluesky", name: "Bluesky card fetcher", tokens: ["bluesky cardyb", "cardyb/", "blueskybot"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2024
2163
|
{ id: "iframely", name: "Iframely", tokens: ["iframely"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2025
2164
|
{ id: "skype-preview", name: "Skype URI preview", tokens: ["skypeuripreview"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2026
2165
|
{ id: "vk-share", name: "VK / Odnoklassniki preview", tokens: ["vkshare", "odklbot"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2027
2166
|
{ id: "discourse-onebox", name: "Discourse Onebox", tokens: ["discourse forum onebox"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2167
|
+
{ id: "microsoft-preview", name: "Microsoft Teams preview", tokens: ["microsoftpreview"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2168
|
+
{ id: "zoom-preview", name: "Zoom link preview", tokens: ["zoombot"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2169
|
+
{ id: "signal-preview", name: "Signal link preview", tokens: ["signalbot"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2170
|
+
{ id: "matrix-synapse", name: "Matrix (Synapse) preview", tokens: ["synapse/"], category: "social", benign: true, verification: { kind: "none" } },
|
|
2028
2171
|
{ id: "yahoo-preview", name: "Yahoo Link Preview", tokens: ["yahoo link preview"], category: "social", benign: true, verification: { kind: "none" } }
|
|
2029
2172
|
];
|
|
2030
2173
|
MONITORING = [
|
|
@@ -2064,6 +2207,10 @@ var init_known_bots = __esm({
|
|
|
2064
2207
|
{ id: "smartnews", name: "SmartNews", tokens: ["smartnewsbot"], category: "feed", benign: true, verification: { kind: "none" } },
|
|
2065
2208
|
{ id: "flipboard", name: "Flipboard", tokens: ["flipboardproxy"], category: "feed", benign: true, verification: { kind: "none" } },
|
|
2066
2209
|
{ id: "podcast-index", name: "Podcast Index", tokens: ["podcastindexbot"], category: "feed", benign: true, verification: { kind: "none" } },
|
|
2210
|
+
// Spotify's podcast fetcher sends `Spotify/1.0` — and so does the Spotify desktop app,
|
|
2211
|
+
// with a person driving it. There is no token that separates them, so this one is left
|
|
2212
|
+
// unnamed rather than named wrongly: the corpus proved the point immediately by blocking
|
|
2213
|
+
// a human under `protect-auth`, `indexers-only` and `under-attack` at once.
|
|
2067
2214
|
{ id: "freshrss", name: "FreshRSS", tokens: ["freshrss"], category: "feed", benign: true, verification: { kind: "none" } },
|
|
2068
2215
|
{ id: "netnewswire", name: "NetNewsWire", tokens: ["netnewswire"], category: "feed", benign: true, verification: { kind: "none" } },
|
|
2069
2216
|
{ id: "overcast", name: "Overcast", tokens: ["overcast/"], category: "feed", benign: true, verification: { kind: "none" } },
|
|
@@ -2190,7 +2337,23 @@ var init_known_bots = __esm({
|
|
|
2190
2337
|
];
|
|
2191
2338
|
ADVERTISING = [
|
|
2192
2339
|
{ id: "adsbot-google", name: "AdsBot-Google", tokens: ["adsbot-google", "mediapartners-google", "adsbot"], category: "advertising", benign: true, verification: { kind: "fcrdns", domains: ["googlebot.com", "google.com"] } },
|
|
2193
|
-
{ id: "criteo", name: "Criteo", tokens: ["criteobot"], category: "advertising", benign: true, verification: { kind: "none" } }
|
|
2340
|
+
{ id: "criteo", name: "Criteo", tokens: ["criteobot"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2341
|
+
// Verification and contextual classification: they read a page to decide whether an ad
|
|
2342
|
+
// may appear beside it, or what the page is about. A publisher usually wants these and a
|
|
2343
|
+
// site with no advertising has no reason to.
|
|
2344
|
+
{ id: "doubleverify", name: "DoubleVerify", tokens: ["doubleverifybot"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2345
|
+
{ id: "ias", name: "Integral Ad Science", tokens: ["ias crawler"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2346
|
+
{ id: "moat", name: "Moat", tokens: ["moatbot"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2347
|
+
{ id: "comscore", name: "comScore (Proximic)", tokens: ["proximic"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2348
|
+
{ id: "grapeshot", name: "Grapeshot", tokens: ["grapeshotcrawler"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2349
|
+
{ id: "peer39", name: "Peer39", tokens: ["peer39bot"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2350
|
+
{ id: "taboola", name: "Taboola", tokens: ["taboolabot"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2351
|
+
{ id: "outbrain", name: "Outbrain", tokens: ["outbrainbot"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2352
|
+
{ id: "pubmatic", name: "PubMatic", tokens: ["pubmaticbot"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2353
|
+
{ id: "thetradedesk", name: "The Trade Desk", tokens: ["ttd-content"], category: "advertising", benign: true, verification: { kind: "none" } },
|
|
2354
|
+
// Competitive ad intelligence rather than verification: it collects what everyone else
|
|
2355
|
+
// is running. Named, and left for the operator to decide about.
|
|
2356
|
+
{ id: "adbeat", name: "Adbeat", tokens: ["adbeat_bot"], category: "advertising", benign: false, verification: { kind: "none" } }
|
|
2194
2357
|
];
|
|
2195
2358
|
COMMERCE = [
|
|
2196
2359
|
{ id: "idealo", name: "idealo", tokens: ["idealo-bot"], category: "commerce", benign: false, verification: { kind: "none" } },
|
|
@@ -2206,6 +2369,12 @@ var init_known_bots = __esm({
|
|
|
2206
2369
|
{ id: "openalex", name: "OpenAlex", tokens: ["openalexbot"], category: "academic", benign: true, verification: { kind: "none" } },
|
|
2207
2370
|
{ id: "webis", name: "Webis research crawler", tokens: ["webisbot"], category: "academic", benign: true, verification: { kind: "none" } }
|
|
2208
2371
|
];
|
|
2372
|
+
EMAIL_SECURITY = [
|
|
2373
|
+
{ id: "proofpoint", name: "Proofpoint URL Defense", tokens: ["proofpointurldefensebot"], category: "email-security", benign: true, verification: { kind: "none" } },
|
|
2374
|
+
{ id: "mimecast", name: "Mimecast URL Protect", tokens: ["mimecasturlprotectbot"], category: "email-security", benign: true, verification: { kind: "none" } },
|
|
2375
|
+
{ id: "barracuda", name: "Barracuda Link Protect", tokens: ["barracudalinkprotectbot"], category: "email-security", benign: true, verification: { kind: "none" } },
|
|
2376
|
+
{ id: "cisco-esa", name: "Cisco Secure Email", tokens: ["ciscosecureemailbot"], category: "email-security", benign: true, verification: { kind: "none" } }
|
|
2377
|
+
];
|
|
2209
2378
|
ACCESSIBILITY = [
|
|
2210
2379
|
{ id: "siteimprove", name: "Siteimprove", tokens: ["siteimprovebot"], category: "accessibility", benign: true, robotsAgent: "SiteimproveBot", verification: { kind: "none" } }
|
|
2211
2380
|
];
|
|
@@ -2224,7 +2393,8 @@ var init_known_bots = __esm({
|
|
|
2224
2393
|
...EMBEDDED,
|
|
2225
2394
|
...COMMERCE,
|
|
2226
2395
|
...ACADEMIC,
|
|
2227
|
-
...ACCESSIBILITY
|
|
2396
|
+
...ACCESSIBILITY,
|
|
2397
|
+
...EMAIL_SECURITY
|
|
2228
2398
|
]);
|
|
2229
2399
|
BENIGN_CATEGORIES = /* @__PURE__ */ new Set(["search", "social", "monitoring", "feed", "archive", "advertising"]);
|
|
2230
2400
|
}
|
|
@@ -2808,6 +2978,166 @@ var init_crawl_breadth = __esm({
|
|
|
2808
2978
|
}
|
|
2809
2979
|
});
|
|
2810
2980
|
|
|
2981
|
+
// src/detectors/parameter-sweep.ts
|
|
2982
|
+
function parameterSweepDetector(options = {}) {
|
|
2983
|
+
const threshold = options.threshold ?? 25;
|
|
2984
|
+
const variantsPerPath = options.variantsPerPath ?? 8;
|
|
2985
|
+
const minRequests = options.minRequests ?? 20;
|
|
2986
|
+
if (threshold > MAX_TRACKED_QUERIES) {
|
|
2987
|
+
throw new RangeError(
|
|
2988
|
+
`parameterSweepDetector threshold ${threshold} can never be reached: an actor's distinct-query count saturates at ${MAX_TRACKED_QUERIES}. Use ${MAX_TRACKED_QUERIES} or fewer.`
|
|
2989
|
+
);
|
|
2990
|
+
}
|
|
2991
|
+
return {
|
|
2992
|
+
id: "parameter-sweep",
|
|
2993
|
+
description: "Counts distinct query strings against the paths they sit on, to catch enumeration that leaves the path unchanged",
|
|
2994
|
+
cost: "cheap",
|
|
2995
|
+
stage: "always",
|
|
2996
|
+
inspect(ctx) {
|
|
2997
|
+
const { distinctPaths, distinctQueries, queriesSaturated, total } = ctx.state;
|
|
2998
|
+
if (total < minRequests || distinctQueries < threshold) return void 0;
|
|
2999
|
+
const spread = distinctQueries / Math.max(1, distinctPaths);
|
|
3000
|
+
if (spread < variantsPerPath) return void 0;
|
|
3001
|
+
return {
|
|
3002
|
+
detector: "parameter-sweep",
|
|
3003
|
+
summary: queriesSaturated ? `at least ${distinctQueries} distinct query strings across only ${distinctPaths} path(s)` : `${distinctQueries} distinct query strings across only ${distinctPaths} path(s) in ${total} requests`,
|
|
3004
|
+
direction: "bot",
|
|
3005
|
+
certainty: "weak",
|
|
3006
|
+
// Saturation means the count stopped being able to grow, so the real spread is
|
|
3007
|
+
// wider than the one reported — the same argument breadth makes for itself.
|
|
3008
|
+
weight: queriesSaturated ? 0.25 : 0.15,
|
|
3009
|
+
botClass: "scraper",
|
|
3010
|
+
metadata: {
|
|
3011
|
+
distinctQueries,
|
|
3012
|
+
distinctPaths,
|
|
3013
|
+
variantsPerPath: Number(spread.toFixed(1)),
|
|
3014
|
+
totalRequests: total,
|
|
3015
|
+
saturated: queriesSaturated
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
};
|
|
3020
|
+
}
|
|
3021
|
+
var init_parameter_sweep = __esm({
|
|
3022
|
+
"src/detectors/parameter-sweep.ts"() {
|
|
3023
|
+
"use strict";
|
|
3024
|
+
init_state();
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
|
|
3028
|
+
// src/detectors/transport-coherence.ts
|
|
3029
|
+
function transportCoherenceDetector(options = {}) {
|
|
3030
|
+
const checkLegacyHttp = options.legacyHttp ?? true;
|
|
3031
|
+
const minHeadRequests = options.minHeadRequests ?? 8;
|
|
3032
|
+
return {
|
|
3033
|
+
id: "transport-coherence",
|
|
3034
|
+
description: "Reads the HTTP version and the methods across a visit against the client the request claims to be",
|
|
3035
|
+
cost: "cheap",
|
|
3036
|
+
stage: "always",
|
|
3037
|
+
inspect(ctx) {
|
|
3038
|
+
if (!claimsBrowser(ctx.ua)) return void 0;
|
|
3039
|
+
const results = [];
|
|
3040
|
+
const version = ctx.facts.httpVersion;
|
|
3041
|
+
if (checkLegacyHttp && version !== void 0 && LEGACY_VERSIONS.has(version)) {
|
|
3042
|
+
results.push({
|
|
3043
|
+
detector: "transport-coherence",
|
|
3044
|
+
summary: `Client claims to be a browser but negotiated HTTP/${version}, which no shipping browser has offered in over a decade`,
|
|
3045
|
+
direction: "bot",
|
|
3046
|
+
certainty: "moderate",
|
|
3047
|
+
botClass: "impersonator",
|
|
3048
|
+
// One downgrading proxy in front of the application does this to every request
|
|
3049
|
+
// that passes through it, so this must count once rather than once per reason.
|
|
3050
|
+
family: "legacy-transport",
|
|
3051
|
+
metadata: { httpVersion: version, browser: ctx.ua.browser }
|
|
3052
|
+
});
|
|
3053
|
+
}
|
|
3054
|
+
const methods = ctx.state.methodsSeen;
|
|
3055
|
+
if (ctx.state.total >= minHeadRequests && methods.length > 0 && methods.every((method) => method === "HEAD")) {
|
|
3056
|
+
results.push({
|
|
3057
|
+
detector: "transport-coherence",
|
|
3058
|
+
summary: `Client claims to be a browser but has issued nothing but HEAD across ${ctx.state.total} requests`,
|
|
3059
|
+
direction: "bot",
|
|
3060
|
+
certainty: "moderate",
|
|
3061
|
+
botClass: "scraper",
|
|
3062
|
+
metadata: { requests: ctx.state.total, browser: ctx.ua.browser }
|
|
3063
|
+
});
|
|
3064
|
+
}
|
|
3065
|
+
return results.length > 0 ? results : void 0;
|
|
3066
|
+
}
|
|
3067
|
+
};
|
|
3068
|
+
}
|
|
3069
|
+
var LEGACY_VERSIONS;
|
|
3070
|
+
var init_transport_coherence = __esm({
|
|
3071
|
+
"src/detectors/transport-coherence.ts"() {
|
|
3072
|
+
"use strict";
|
|
3073
|
+
init_ua();
|
|
3074
|
+
LEGACY_VERSIONS = /* @__PURE__ */ new Set(["0.9", "1.0"]);
|
|
3075
|
+
}
|
|
3076
|
+
});
|
|
3077
|
+
|
|
3078
|
+
// src/detectors/probe-volume.ts
|
|
3079
|
+
function probeVolumeDetector(options = {}) {
|
|
3080
|
+
const minResponses = options.minResponses ?? 20;
|
|
3081
|
+
const missRatio = options.missRatio ?? 0.8;
|
|
3082
|
+
return {
|
|
3083
|
+
id: "probe-volume",
|
|
3084
|
+
description: "Reads the share of an actor's requests that were answered 404 or 410, where the application reports them",
|
|
3085
|
+
cost: "cheap",
|
|
3086
|
+
stage: "always",
|
|
3087
|
+
inspect(ctx) {
|
|
3088
|
+
const { responses, misses } = ctx.state;
|
|
3089
|
+
if (responses < minResponses) return void 0;
|
|
3090
|
+
const ratio = misses / responses;
|
|
3091
|
+
if (ratio < missRatio) return void 0;
|
|
3092
|
+
return {
|
|
3093
|
+
detector: "probe-volume",
|
|
3094
|
+
summary: `${misses} of this client's last ${responses} requests were answered "not found" (${(ratio * 100).toFixed(0)}%)`,
|
|
3095
|
+
direction: "bot",
|
|
3096
|
+
certainty: "moderate",
|
|
3097
|
+
botClass: "scanner",
|
|
3098
|
+
metadata: { responses, misses, missRatio: Number(ratio.toFixed(3)) }
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
}
|
|
3103
|
+
var init_probe_volume = __esm({
|
|
3104
|
+
"src/detectors/probe-volume.ts"() {
|
|
3105
|
+
"use strict";
|
|
3106
|
+
}
|
|
3107
|
+
});
|
|
3108
|
+
|
|
3109
|
+
// src/detectors/id-enumeration.ts
|
|
3110
|
+
function idEnumerationDetector(options = {}) {
|
|
3111
|
+
const minRequests = options.minRequests ?? 30;
|
|
3112
|
+
const density = options.density ?? 0.9;
|
|
3113
|
+
return {
|
|
3114
|
+
id: "id-enumeration",
|
|
3115
|
+
description: "Reports an actor covering a contiguous range of numeric identifiers under one path shape",
|
|
3116
|
+
cost: "cheap",
|
|
3117
|
+
stage: "always",
|
|
3118
|
+
inspect(ctx) {
|
|
3119
|
+
const walk = ctx.state.densestWalk();
|
|
3120
|
+
if (walk === void 0 || walk.count < minRequests) return void 0;
|
|
3121
|
+
if (walk.span < minRequests) return void 0;
|
|
3122
|
+
const covered = Math.min(1, walk.count / walk.span);
|
|
3123
|
+
if (covered < density) return void 0;
|
|
3124
|
+
return {
|
|
3125
|
+
detector: "id-enumeration",
|
|
3126
|
+
summary: `${walk.count} requests to ${walk.template} covering ${(covered * 100).toFixed(0)}% of a ${walk.span}-wide range of ids`,
|
|
3127
|
+
direction: "bot",
|
|
3128
|
+
certainty: "moderate",
|
|
3129
|
+
botClass: "scraper",
|
|
3130
|
+
metadata: { template: walk.template, requests: walk.count, span: walk.span, coverage: Number(covered.toFixed(3)) }
|
|
3131
|
+
};
|
|
3132
|
+
}
|
|
3133
|
+
};
|
|
3134
|
+
}
|
|
3135
|
+
var init_id_enumeration = __esm({
|
|
3136
|
+
"src/detectors/id-enumeration.ts"() {
|
|
3137
|
+
"use strict";
|
|
3138
|
+
}
|
|
3139
|
+
});
|
|
3140
|
+
|
|
2811
3141
|
// src/detectors/crawler-verification.ts
|
|
2812
3142
|
function crawlerVerificationDetector(options = {}) {
|
|
2813
3143
|
const missingPtrIsForgery = options.treatMissingPtrAsForgery ?? true;
|
|
@@ -4185,11 +4515,15 @@ function defaultDetectors(options = {}) {
|
|
|
4185
4515
|
clientHintsDetector(),
|
|
4186
4516
|
fetchMetadataDetector(),
|
|
4187
4517
|
acceptSignatureDetector(),
|
|
4518
|
+
transportCoherenceDetector(),
|
|
4188
4519
|
headerOrderDetector(),
|
|
4189
4520
|
// Behaviour across requests.
|
|
4190
4521
|
rateAnomalyDetector(),
|
|
4191
4522
|
cadenceDetector(),
|
|
4192
4523
|
crawlBreadthDetector(),
|
|
4524
|
+
parameterSweepDetector(),
|
|
4525
|
+
probeVolumeDetector(),
|
|
4526
|
+
idEnumerationDetector(),
|
|
4193
4527
|
sessionIntegrityDetector(),
|
|
4194
4528
|
// The other side of the argument: what a real browsing session looks like.
|
|
4195
4529
|
browsingCoherenceDetector(),
|
|
@@ -4205,6 +4539,10 @@ var init_detectors = __esm({
|
|
|
4205
4539
|
init_cadence();
|
|
4206
4540
|
init_client_hints();
|
|
4207
4541
|
init_crawl_breadth();
|
|
4542
|
+
init_parameter_sweep();
|
|
4543
|
+
init_transport_coherence();
|
|
4544
|
+
init_probe_volume();
|
|
4545
|
+
init_id_enumeration();
|
|
4208
4546
|
init_crawler_verification();
|
|
4209
4547
|
init_fetch_metadata();
|
|
4210
4548
|
init_header_integrity();
|
|
@@ -4227,6 +4565,10 @@ var init_detectors = __esm({
|
|
|
4227
4565
|
init_rate_anomaly();
|
|
4228
4566
|
init_cadence();
|
|
4229
4567
|
init_crawl_breadth();
|
|
4568
|
+
init_parameter_sweep();
|
|
4569
|
+
init_transport_coherence();
|
|
4570
|
+
init_probe_volume();
|
|
4571
|
+
init_id_enumeration();
|
|
4230
4572
|
init_session_integrity();
|
|
4231
4573
|
init_identity_rotation();
|
|
4232
4574
|
init_trap();
|
|
@@ -5054,6 +5396,11 @@ function assessmentFromEntry(entry) {
|
|
|
5054
5396
|
key: entry.actor,
|
|
5055
5397
|
requests: entry.actorStats.requests,
|
|
5056
5398
|
distinctPaths: entry.actorStats.distinctPaths,
|
|
5399
|
+
distinctQueries: 0,
|
|
5400
|
+
queriesSaturated: false,
|
|
5401
|
+
methodsSeen: ["GET"],
|
|
5402
|
+
responses: 0,
|
|
5403
|
+
misses: 0,
|
|
5057
5404
|
firstSeen: entry.actorStats.firstSeen,
|
|
5058
5405
|
lastSeen: entry.at,
|
|
5059
5406
|
...entry.actorStats.sinceLastMs !== void 0 ? { sinceLastMs: entry.actorStats.sinceLastMs } : {},
|
|
@@ -5322,7 +5669,10 @@ button[disabled] { opacity: .5; cursor: default; }
|
|
|
5322
5669
|
.tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--s1); }
|
|
5323
5670
|
|
|
5324
5671
|
/* --- layout ------------------------------------------------------------- */
|
|
5325
|
-
|
|
5672
|
+
/* The top padding is the gap under the sticky header. At 18px the counter row sat almost
|
|
5673
|
+
against the header's border and read as part of it; the tiles carry their own border, so
|
|
5674
|
+
two lines were meeting with nothing between them. */
|
|
5675
|
+
main { padding: 28px 20px 64px; max-width: 1680px; margin: 0 auto; }
|
|
5326
5676
|
.stack { display: grid; gap: 16px; }
|
|
5327
5677
|
/* Everything above the feed is drawn by script once the first snapshot arrives, which
|
|
5328
5678
|
inserts a block of content above what is already laid out. The browser's scroll
|
|
@@ -7536,6 +7886,7 @@ __export(index_exports, {
|
|
|
7536
7886
|
defineHandler: () => defineHandler,
|
|
7537
7887
|
evidence: () => evidence,
|
|
7538
7888
|
executeAction: () => executeAction,
|
|
7889
|
+
fetchAddressList: () => fetchAddressList,
|
|
7539
7890
|
fetchCrawlerRanges: () => fetchCrawlerRanges,
|
|
7540
7891
|
fetchMetadataDetector: () => fetchMetadataDetector,
|
|
7541
7892
|
formatIp: () => formatIp,
|
|
@@ -7544,6 +7895,7 @@ __export(index_exports, {
|
|
|
7544
7895
|
headerIntegrityDetector: () => headerIntegrityDetector,
|
|
7545
7896
|
headerOrderDetector: () => headerOrderDetector,
|
|
7546
7897
|
headerOrderFingerprint: () => headerOrderFingerprint,
|
|
7898
|
+
idEnumerationDetector: () => idEnumerationDetector,
|
|
7547
7899
|
identityRotationDetector: () => identityRotationDetector,
|
|
7548
7900
|
independentStrongSignals: () => independentStrongSignals,
|
|
7549
7901
|
indexSignatures: () => indexSignatures,
|
|
@@ -7559,6 +7911,7 @@ __export(index_exports, {
|
|
|
7559
7911
|
noisyOr: () => noisyOr,
|
|
7560
7912
|
normalizeIp: () => normalizeIp,
|
|
7561
7913
|
notifyJsNotifier: () => notifyJsNotifier,
|
|
7914
|
+
parameterSweepDetector: () => parameterSweepDetector,
|
|
7562
7915
|
parseAcceptLanguage: () => parseAcceptLanguage,
|
|
7563
7916
|
parseCidr: () => parseCidr,
|
|
7564
7917
|
parseCookies: () => parseCookies,
|
|
@@ -7568,6 +7921,7 @@ __export(index_exports, {
|
|
|
7568
7921
|
pickTranslation: () => pickTranslation,
|
|
7569
7922
|
probeShapeFor: () => probeShapeFor,
|
|
7570
7923
|
probeSignatureDetector: () => probeSignatureDetector,
|
|
7924
|
+
probeVolumeDetector: () => probeVolumeDetector,
|
|
7571
7925
|
protectApi: () => protectApi,
|
|
7572
7926
|
protectAuth: () => protectAuth,
|
|
7573
7927
|
protectContent: () => protectContent,
|
|
@@ -7596,6 +7950,7 @@ __export(index_exports, {
|
|
|
7596
7950
|
systemClock: () => systemClock,
|
|
7597
7951
|
tlsFingerprintDetector: () => tlsFingerprintDetector,
|
|
7598
7952
|
toPrometheus: () => toPrometheus,
|
|
7953
|
+
transportCoherenceDetector: () => transportCoherenceDetector,
|
|
7599
7954
|
trapDetector: () => trapDetector,
|
|
7600
7955
|
trapRobotsEntries: () => trapRobotsEntries,
|
|
7601
7956
|
uaCoherenceDetector: () => uaCoherenceDetector,
|
|
@@ -9457,6 +9812,24 @@ var BotHandler = class {
|
|
|
9457
9812
|
this.warn(`Actor "${key}" was cleared as human at runtime${attribute(context)}, until ${new Date(until).toISOString()}.`);
|
|
9458
9813
|
this.events.emit("actor-change", { key, action: "clear", until, by: context.by });
|
|
9459
9814
|
}
|
|
9815
|
+
/**
|
|
9816
|
+
* Tells the engine what the application answered.
|
|
9817
|
+
*
|
|
9818
|
+
* The one thing detection cannot see for itself. Every verdict here is reached *before*
|
|
9819
|
+
* the response exists — that is what makes it useful, since it can shape the response —
|
|
9820
|
+
* and so the status is knowledge only the application holds. Handed back, it closes the
|
|
9821
|
+
* oldest gap in reading a scanner: an actor whose requests are almost all misses is
|
|
9822
|
+
* looking for something rather than reading anything, and no amount of header analysis
|
|
9823
|
+
* shows that.
|
|
9824
|
+
*
|
|
9825
|
+
* Optional, and silent when the actor has already been forgotten. Nothing about
|
|
9826
|
+
* detection depends on it being called; supplying it sharpens `probe-volume` and
|
|
9827
|
+
* nothing else. The bundled Node adapter wires it up for you.
|
|
9828
|
+
*/
|
|
9829
|
+
recordOutcome(facts, status) {
|
|
9830
|
+
if (!Number.isFinite(status)) return;
|
|
9831
|
+
this.registry.peek(this.actorKeyFor(facts))?.recordOutcome(status);
|
|
9832
|
+
}
|
|
9460
9833
|
/** Convenience for `updateRanges("crawler:<id>", …)`, matching a signature id. */
|
|
9461
9834
|
updateCrawlerRanges(signatureId, entries, context = {}) {
|
|
9462
9835
|
this.updateRanges(`crawler:${signatureId}`, entries, context);
|
|
@@ -9846,6 +10219,11 @@ var BotHandler = class {
|
|
|
9846
10219
|
key: actorKey,
|
|
9847
10220
|
requests: 0,
|
|
9848
10221
|
distinctPaths: 0,
|
|
10222
|
+
distinctQueries: 0,
|
|
10223
|
+
queriesSaturated: false,
|
|
10224
|
+
methodsSeen: ["GET"],
|
|
10225
|
+
responses: 0,
|
|
10226
|
+
misses: 0,
|
|
9849
10227
|
firstSeen: facts.timestamp,
|
|
9850
10228
|
lastSeen: facts.timestamp,
|
|
9851
10229
|
priorConfirmations: 0,
|
|
@@ -9917,6 +10295,12 @@ var MAX_V6_BLOCK = 19;
|
|
|
9917
10295
|
var MAX_PREFIXES = 1e4;
|
|
9918
10296
|
var MAX_BYTES = 4 * 1024 * 1024;
|
|
9919
10297
|
async function fetchCrawlerRanges(source, options = {}) {
|
|
10298
|
+
return fetchPrefixes(source, options, { maxPrefixes: MAX_PREFIXES, subject: `a crawler's address list`, id: source.id });
|
|
10299
|
+
}
|
|
10300
|
+
async function fetchAddressList(source, options = {}) {
|
|
10301
|
+
return fetchPrefixes(source, options, { maxPrefixes: options.maxPrefixes ?? 1e5, subject: "an address list", id: source.id });
|
|
10302
|
+
}
|
|
10303
|
+
async function fetchPrefixes(source, options, limits) {
|
|
9920
10304
|
const url = new URL(source.url);
|
|
9921
10305
|
if (url.protocol !== "https:") throw new ConfigError(`Crawler ranges must be published over HTTPS. "${source.url}" is not.`);
|
|
9922
10306
|
const fetcher = options.fetch ?? globalThis.fetch;
|
|
@@ -9930,7 +10314,7 @@ async function fetchCrawlerRanges(source, options = {}) {
|
|
|
9930
10314
|
const text = await response.text();
|
|
9931
10315
|
if (text.length > MAX_BYTES) throw new Error(`the list is ${Math.round(text.length / 1024)} kB, which is not a list of prefixes`);
|
|
9932
10316
|
const prefixes = text.trimStart().startsWith("{") ? fromJson(text) : fromLines(text);
|
|
9933
|
-
return validate(prefixes,
|
|
10317
|
+
return validate(prefixes, limits);
|
|
9934
10318
|
}
|
|
9935
10319
|
function fromJson(text) {
|
|
9936
10320
|
const document = JSON.parse(text);
|
|
@@ -9943,24 +10327,24 @@ function fromJson(text) {
|
|
|
9943
10327
|
return out;
|
|
9944
10328
|
}
|
|
9945
10329
|
function fromLines(text) {
|
|
9946
|
-
return text.split("\n").map((line) => line.split("#")[0]?.trim() ?? "").filter((line) => line !== "");
|
|
10330
|
+
return text.split("\n").map((line) => (line.split("#")[0] ?? "").split(";")[0]?.trim() ?? "").filter((line) => line !== "");
|
|
9947
10331
|
}
|
|
9948
|
-
function validate(prefixes,
|
|
10332
|
+
function validate(prefixes, limits) {
|
|
9949
10333
|
if (prefixes.length === 0) throw new Error("the list is empty");
|
|
9950
|
-
if (prefixes.length >
|
|
10334
|
+
if (prefixes.length > limits.maxPrefixes) throw new Error(`${prefixes.length} prefixes is not ${limits.subject}`);
|
|
9951
10335
|
const accepted = [];
|
|
9952
10336
|
for (const prefix of prefixes) {
|
|
9953
10337
|
const cidr = parseCidr(prefix);
|
|
9954
10338
|
if (cidr === void 0 || cidr === null) continue;
|
|
9955
10339
|
const isV4 = cidr.bytes.length === 4;
|
|
9956
10340
|
if (cidr.prefix < (isV4 ? MAX_V4_BLOCK : MAX_V6_BLOCK)) {
|
|
9957
|
-
throw new Error(`"${prefix}" covers more of the internet than any
|
|
10341
|
+
throw new Error(`"${prefix}" covers more of the internet than any published list should \u2014 refusing the whole list rather than acting on it`);
|
|
9958
10342
|
}
|
|
9959
10343
|
accepted.push(prefix);
|
|
9960
10344
|
}
|
|
9961
10345
|
if (accepted.length === 0) throw new Error(`nothing in the list parsed as an address or CIDR (first entry: "${prefixes[0]}")`);
|
|
9962
10346
|
const set = new IpRangeSet(accepted);
|
|
9963
|
-
if (set.size === 0) throw new Error(`nothing in the list loaded for ${id}`);
|
|
10347
|
+
if (set.size === 0) throw new Error(`nothing in the list loaded for ${limits.id}`);
|
|
9964
10348
|
return accepted;
|
|
9965
10349
|
}
|
|
9966
10350
|
async function refreshCrawlerRanges(handler, options = {}) {
|
|
@@ -10274,6 +10658,7 @@ init_lru();
|
|
|
10274
10658
|
defineHandler,
|
|
10275
10659
|
evidence,
|
|
10276
10660
|
executeAction,
|
|
10661
|
+
fetchAddressList,
|
|
10277
10662
|
fetchCrawlerRanges,
|
|
10278
10663
|
fetchMetadataDetector,
|
|
10279
10664
|
formatIp,
|
|
@@ -10282,6 +10667,7 @@ init_lru();
|
|
|
10282
10667
|
headerIntegrityDetector,
|
|
10283
10668
|
headerOrderDetector,
|
|
10284
10669
|
headerOrderFingerprint,
|
|
10670
|
+
idEnumerationDetector,
|
|
10285
10671
|
identityRotationDetector,
|
|
10286
10672
|
independentStrongSignals,
|
|
10287
10673
|
indexSignatures,
|
|
@@ -10297,6 +10683,7 @@ init_lru();
|
|
|
10297
10683
|
noisyOr,
|
|
10298
10684
|
normalizeIp,
|
|
10299
10685
|
notifyJsNotifier,
|
|
10686
|
+
parameterSweepDetector,
|
|
10300
10687
|
parseAcceptLanguage,
|
|
10301
10688
|
parseCidr,
|
|
10302
10689
|
parseCookies,
|
|
@@ -10306,6 +10693,7 @@ init_lru();
|
|
|
10306
10693
|
pickTranslation,
|
|
10307
10694
|
probeShapeFor,
|
|
10308
10695
|
probeSignatureDetector,
|
|
10696
|
+
probeVolumeDetector,
|
|
10309
10697
|
protectApi,
|
|
10310
10698
|
protectAuth,
|
|
10311
10699
|
protectContent,
|
|
@@ -10334,6 +10722,7 @@ init_lru();
|
|
|
10334
10722
|
systemClock,
|
|
10335
10723
|
tlsFingerprintDetector,
|
|
10336
10724
|
toPrometheus,
|
|
10725
|
+
transportCoherenceDetector,
|
|
10337
10726
|
trapDetector,
|
|
10338
10727
|
trapRobotsEntries,
|
|
10339
10728
|
uaCoherenceDetector,
|