@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/core.d.ts
CHANGED
|
@@ -311,6 +311,21 @@ export declare class BotHandler {
|
|
|
311
311
|
* `isHuman`, and it expires the same way a solved challenge does.
|
|
312
312
|
*/
|
|
313
313
|
clearActor(key: string, forMs: number, context?: ChangeContext): void;
|
|
314
|
+
/**
|
|
315
|
+
* Tells the engine what the application answered.
|
|
316
|
+
*
|
|
317
|
+
* The one thing detection cannot see for itself. Every verdict here is reached *before*
|
|
318
|
+
* the response exists — that is what makes it useful, since it can shape the response —
|
|
319
|
+
* and so the status is knowledge only the application holds. Handed back, it closes the
|
|
320
|
+
* oldest gap in reading a scanner: an actor whose requests are almost all misses is
|
|
321
|
+
* looking for something rather than reading anything, and no amount of header analysis
|
|
322
|
+
* shows that.
|
|
323
|
+
*
|
|
324
|
+
* Optional, and silent when the actor has already been forgotten. Nothing about
|
|
325
|
+
* detection depends on it being called; supplying it sharpens `probe-volume` and
|
|
326
|
+
* nothing else. The bundled Node adapter wires it up for you.
|
|
327
|
+
*/
|
|
328
|
+
recordOutcome(facts: RequestFacts, status: number): void;
|
|
314
329
|
/** Convenience for `updateRanges("crawler:<id>", …)`, matching a signature id. */
|
|
315
330
|
updateCrawlerRanges(signatureId: string, entries: readonly string[], context?: ChangeContext): void;
|
|
316
331
|
/**
|
package/dist/corpus/index.cjs
CHANGED
|
@@ -1208,6 +1208,89 @@ var AI_CRAWLER_CASES = [
|
|
|
1208
1208
|
// src/corpus/adversarial.ts
|
|
1209
1209
|
var CHROME_UA = userAgentOf("chromeWindows");
|
|
1210
1210
|
var ADVERSARIAL_CASES = [
|
|
1211
|
+
bot({
|
|
1212
|
+
id: "id-harvest-contiguous",
|
|
1213
|
+
title: "Every profile id in order, with a copied browser header set",
|
|
1214
|
+
audience: "hostile",
|
|
1215
|
+
category: "scraping",
|
|
1216
|
+
provenance: "Harvesting by identifier rather than by link: the shape of an IDOR sweep and of profile collection. Distinct-path breadth reads it as somebody who visited a lot of pages, which is also what it reads when a person works through a documentation site.",
|
|
1217
|
+
requests: repeat({ ...browser("chromeWindows"), ip: "198.51.100.65" }, 40, 800, (index) => `/user/${index + 1}`),
|
|
1218
|
+
expect: {
|
|
1219
|
+
// One `moderate` signal against a flawless header set, like the others here. What
|
|
1220
|
+
// changed is that the walk is now *visible* — before this detector it was scored
|
|
1221
|
+
// identically to a hundred and twenty scattered ids and to ordinary article paths.
|
|
1222
|
+
verdict: "unknown",
|
|
1223
|
+
detectors: ["id-enumeration"]
|
|
1224
|
+
},
|
|
1225
|
+
notes: "What separates this from reading is not which ids were asked for but that they cover a range: people arrive at ids through links, and links do not densely enumerate an integer interval. Held at `moderate` because products in one category often carry consecutive ids, so somebody browsing a catalogue makes a smaller version of this shape."
|
|
1226
|
+
}),
|
|
1227
|
+
bot({
|
|
1228
|
+
id: "wordlist-scan-mostly-misses",
|
|
1229
|
+
title: "A wordlist walked with a copied browser header set, almost all of it missing",
|
|
1230
|
+
audience: "hostile",
|
|
1231
|
+
category: "scanning",
|
|
1232
|
+
provenance: "The oldest tell there is, and the one this library could not see: it decides before the response exists, which is what lets it shape the response and also what hides the status from it. A person browsing does not generate thirty misses in a row; a wordlist does almost nothing else.",
|
|
1233
|
+
requests: repeat({ ...browser("chromeWindows"), ip: "198.51.100.64", status: 404 }, 30, 700, (index) => `/${["admin", "backup", "old", "test", "config", "db"][index % 6]}-${index}`),
|
|
1234
|
+
expect: {
|
|
1235
|
+
// One `moderate` signal against an otherwise flawless header set does not cross the
|
|
1236
|
+
// line, and it should not: a site that has just moved its URLs produces the same
|
|
1237
|
+
// shape from ordinary readers. Raising the ceiling so this case reads better would
|
|
1238
|
+
// be tuning the detector to the test rather than to the traffic.
|
|
1239
|
+
verdict: "unknown",
|
|
1240
|
+
detectors: ["probe-volume"]
|
|
1241
|
+
},
|
|
1242
|
+
notes: "Only counts 404 and 410. A 403 is usually this library's own doing, and counting it would let a rule that challenges an actor manufacture the evidence for having challenged it; a 500 is the site's problem and says nothing about the client. Capped at `moderate` because a site that has just moved its URLs produces this from perfectly ordinary readers."
|
|
1243
|
+
}),
|
|
1244
|
+
bot({
|
|
1245
|
+
id: "browser-claim-over-http-1-0",
|
|
1246
|
+
title: "A perfect Chrome header set, arriving over HTTP/1.0",
|
|
1247
|
+
audience: "hostile",
|
|
1248
|
+
category: "impersonation",
|
|
1249
|
+
provenance: "Most tooling lets you set headers and does not let you choose an HTTP version, so the transport is the half a copied header set does not cover. No shipping browser has offered HTTP/1.0 to a server in well over a decade.",
|
|
1250
|
+
requests: repeat({ ...browser("chromeWindows"), ip: "198.51.100.62", httpVersion: "1.0" }, 30, 900, (index) => `/${["news", "about", "blog", "help", "terms"][index % 5]}`),
|
|
1251
|
+
expect: {
|
|
1252
|
+
// Contributes rather than concludes. On its own, against an otherwise flawless
|
|
1253
|
+
// header set, one `moderate` signal does not reach the threshold — and it should
|
|
1254
|
+
// not, because an intermediary can cause this. Beside anything sharper it does.
|
|
1255
|
+
verdict: "unknown",
|
|
1256
|
+
detectors: ["transport-coherence"]
|
|
1257
|
+
},
|
|
1258
|
+
notes: "Capped at `moderate` because it is not always the client's doing: a few older load balancers speak HTTP/1.0 to the origin, and behind one of those every request looks like this. That is what `transportCoherenceDetector({ legacyHttp: false })` is for, and why this may never deny anybody on its own."
|
|
1259
|
+
}),
|
|
1260
|
+
bot({
|
|
1261
|
+
id: "head-only-visit",
|
|
1262
|
+
title: "A visit made entirely of HEAD, claiming a browser",
|
|
1263
|
+
audience: "unwanted-bot",
|
|
1264
|
+
category: "scraping",
|
|
1265
|
+
provenance: "Checking what exists without reading any of it: link checkers, availability monitors and inventory watchers all do this, and a browser navigating never does.",
|
|
1266
|
+
requests: repeat({ ...browser("chromeWindows"), ip: "198.51.100.63", method: "HEAD" }, 30, 900, (index) => `/${["news", "about", "blog", "help", "terms"][index % 5]}`),
|
|
1267
|
+
expect: {
|
|
1268
|
+
// As above: a shape worth reporting, not worth concluding from alone.
|
|
1269
|
+
verdict: "unknown",
|
|
1270
|
+
detectors: ["transport-coherence"]
|
|
1271
|
+
},
|
|
1272
|
+
notes: "One HEAD is a browser checking a link it is about to follow, or a cache revalidating; the shape only means anything across a visit, which is why it is counted on the actor rather than on the request. A link checker is a real and mostly harmless thing to be, so this stays `moderate`."
|
|
1273
|
+
}),
|
|
1274
|
+
bot({
|
|
1275
|
+
id: "catalogue-sweep-by-page",
|
|
1276
|
+
title: "A catalogue taken a page at a time, with the path never changing",
|
|
1277
|
+
audience: "unwanted-bot",
|
|
1278
|
+
category: "scraping",
|
|
1279
|
+
provenance: "How a catalogue is actually taken. The collector copies a browser's headers exactly and walks ?page=1..N, which leaves the path constant \u2014 so distinct-path breadth reads it as somebody rereading one page rather than as enumeration.",
|
|
1280
|
+
requests: repeat({ ...browser("chromeWindows"), ip: "198.51.100.61" }, 40, 900, (index) => `/products?page=${index}`),
|
|
1281
|
+
expect: {
|
|
1282
|
+
// Not proven, and not even suspected at this pace. Said plainly because it is true:
|
|
1283
|
+
// headers this clean leave only behaviour, behaviour is weak by construction, and a
|
|
1284
|
+
// collector polite enough to space its requests stays under the line. What changed
|
|
1285
|
+
// is that it no longer scores *lower* than the identical crawl expressed as distinct
|
|
1286
|
+
// paths — measured at a faster pace before this detector existed, the two differed by
|
|
1287
|
+
// seven points and only the path version crossed; they now score the same at every
|
|
1288
|
+
// volume tried.
|
|
1289
|
+
verdict: "unknown",
|
|
1290
|
+
detectors: ["parameter-sweep"]
|
|
1291
|
+
},
|
|
1292
|
+
notes: "The counterpart to crawl-breadth rather than a replacement for it: breadth counts paths, this counts what is hung on them. Both stay weak, and both are worth having because a collector picks one shape or the other and nothing says which. Neither is a reason to deny anybody on its own."
|
|
1293
|
+
}),
|
|
1211
1294
|
// ---------------------------------------------------------------------------
|
|
1212
1295
|
// Forged identities. The narrow case where a lie is provable.
|
|
1213
1296
|
// ---------------------------------------------------------------------------
|
|
@@ -4033,6 +4116,7 @@ async function runCase(handler, clock, item, startedAt, provides, assertActions
|
|
|
4033
4116
|
const withClearance = clearanceCookie === void 0 ? request : { ...request, headers: [...request.headers, ["Cookie", clearanceCookie]] };
|
|
4034
4117
|
const facts = toFacts(withClearance, fallbackIp, clock.now());
|
|
4035
4118
|
const { assessment, decision, outcome } = await handler.handle(facts);
|
|
4119
|
+
if (request.status !== void 0) handler.recordOutcome(facts, request.status);
|
|
4036
4120
|
requests.push({ assessment, decision, outcome });
|
|
4037
4121
|
}
|
|
4038
4122
|
const final = requests[requests.length - 1];
|