@osqd/bothandlerjs 0.4.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 +634 -25
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +634 -25
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +10 -0
- 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/dashboard/client/feed.d.ts +16 -0
- package/dist/dashboard/client/format.d.ts +18 -0
- package/dist/dashboard/client/pager.d.ts +32 -0
- package/dist/dashboard/client/store.d.ts +60 -0
- package/dist/dashboard/client.generated.d.ts +1 -1
- package/dist/detectors/crawler-verification.d.ts +34 -1
- package/dist/detectors/id-enumeration.d.ts +31 -0
- package/dist/detectors/index.d.ts +12 -1
- package/dist/detectors/known-bots.d.ts +60 -1
- 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 +349 -40
- package/dist/element/index.cjs.map +1 -1
- package/dist/element/index.js +349 -40
- package/dist/element/index.js.map +1 -1
- package/dist/index.cjs +576 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +571 -31
- package/dist/index.js.map +1 -1
- package/dist/state.d.ts +76 -1
- 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 +44 -0
- package/docs/operations/dashboard.md +26 -0
- package/package.json +1 -1
|
@@ -1,5 +1,38 @@
|
|
|
1
|
-
import type { Detector } from "./types.js";
|
|
1
|
+
import type { Detector, DetectionContext } from "./types.js";
|
|
2
|
+
import type { BotSignature } from "./known-bots.js";
|
|
3
|
+
/**
|
|
4
|
+
* What an operator's own check concluded about a claimed identity.
|
|
5
|
+
*
|
|
6
|
+
* Three answers, and the third is not a formality. "I could not tell" has to be
|
|
7
|
+
* expressible and has to mean *silence* — a verifier that returned false for both "this
|
|
8
|
+
* is a forgery" and "my key server timed out" would turn an outage into an accusation.
|
|
9
|
+
*/
|
|
10
|
+
export type VerificationOutcome = "verified" | "refuted" | "unknown";
|
|
11
|
+
/**
|
|
12
|
+
* Your own answer to "is this really who it says it is".
|
|
13
|
+
*
|
|
14
|
+
* Called with the same context the built-in checks get, for one claimed signature. It
|
|
15
|
+
* may be async: the natural implementations are a lookup or a signature check.
|
|
16
|
+
*/
|
|
17
|
+
export type CrawlerVerifier = (ctx: DetectionContext, signature: BotSignature) => VerificationOutcome | Promise<VerificationOutcome>;
|
|
2
18
|
export interface CrawlerVerificationOptions {
|
|
19
|
+
/**
|
|
20
|
+
* Verifiers of your own, by signature id — `{ googlebot: ..., gptbot: ... }`.
|
|
21
|
+
*
|
|
22
|
+
* Most of this database cannot be checked from inside a request: the operator
|
|
23
|
+
* publishes no DNS proof and no range list, and the claim is simply unfalsifiable.
|
|
24
|
+
* That is most bots, and until now it meant the library had nothing to offer an
|
|
25
|
+
* operator who *could* check — because their CDN had already verified the crawler and
|
|
26
|
+
* said so in a header, because the bot signs its requests, or because they hold the
|
|
27
|
+
* ASN data. Writing a whole detector to say so meant reimplementing the confirm and
|
|
28
|
+
* refute semantics in this file, including the part where an inconclusive answer must
|
|
29
|
+
* stay silent.
|
|
30
|
+
*
|
|
31
|
+
* A verifier here runs before the built-in check for that signature and a definite
|
|
32
|
+
* answer settles it, which also means no DNS lookup. `unknown` falls through to
|
|
33
|
+
* whatever this library can do on its own.
|
|
34
|
+
*/
|
|
35
|
+
verifiers?: Readonly<Record<string, CrawlerVerifier>>;
|
|
3
36
|
/**
|
|
4
37
|
* Treat an address with no PTR record as a forged claim. Default true.
|
|
5
38
|
*
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Detector } from "./types.js";
|
|
2
|
+
export interface IdEnumerationOptions {
|
|
3
|
+
/** Requests to one path shape before a walk is worth reporting. Default 30. */
|
|
4
|
+
minRequests?: number;
|
|
5
|
+
/**
|
|
6
|
+
* How completely those requests must cover the range they span. Default 0.9 — thirty
|
|
7
|
+
* requests reaching from id 1 to id 33 report; the same thirty scattered across a
|
|
8
|
+
* thousand ids do not.
|
|
9
|
+
*/
|
|
10
|
+
density?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Somebody working through the identifiers rather than following the links.
|
|
14
|
+
*
|
|
15
|
+
* `crawl-breadth` sees this as "many distinct paths", which is what it also sees when a
|
|
16
|
+
* person reads a documentation site — so it stays `weak` and nothing separates the two.
|
|
17
|
+
* Measured: `/user/1` through `/user/120` in order scored exactly the same as a hundred
|
|
18
|
+
* and twenty scattered ids, and the same again as ordinary article paths. All three
|
|
19
|
+
* `unknown`, all three 57.
|
|
20
|
+
*
|
|
21
|
+
* What separates them is not which ids were asked for but whether they *cover a range*.
|
|
22
|
+
* People arrive at ids through links, and links do not densely enumerate an integer
|
|
23
|
+
* interval; a harvester does nothing else. Thirty requests reaching from id 1 to id 33 is
|
|
24
|
+
* a walk. Thirty scattered across a hundred thousand is somebody reading.
|
|
25
|
+
*
|
|
26
|
+
* `moderate`, and the bar is set high on purpose. The awkward case is real: products in
|
|
27
|
+
* one category often carry consecutive ids, so somebody browsing a catalogue can produce a
|
|
28
|
+
* smaller version of this. Thirty requests covering ninety per cent of their own span is
|
|
29
|
+
* meant to be past what that produces, and it is still a shape rather than a motive.
|
|
30
|
+
*/
|
|
31
|
+
export declare function idEnumerationDetector(options?: IdEnumerationOptions): Detector;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CrawlerVerificationOptions } from "./crawler-verification.js";
|
|
1
2
|
import type { Detector } from "./types.js";
|
|
2
3
|
export type { DetectionContext, Detector, DetectorResult } from "./types.js";
|
|
3
4
|
export { evidence, absenceIsMeaningful } from "./types.js";
|
|
@@ -17,7 +18,15 @@ export type { RateAnomalyOptions } from "./rate-anomaly.js";
|
|
|
17
18
|
export { cadenceDetector } from "./cadence.js";
|
|
18
19
|
export type { CadenceOptions } from "./cadence.js";
|
|
19
20
|
export { crawlBreadthDetector } from "./crawl-breadth.js";
|
|
21
|
+
export { parameterSweepDetector } from "./parameter-sweep.js";
|
|
22
|
+
export { transportCoherenceDetector } from "./transport-coherence.js";
|
|
23
|
+
export { probeVolumeDetector } from "./probe-volume.js";
|
|
24
|
+
export { idEnumerationDetector } from "./id-enumeration.js";
|
|
20
25
|
export type { CrawlBreadthOptions } from "./crawl-breadth.js";
|
|
26
|
+
export type { ParameterSweepOptions } from "./parameter-sweep.js";
|
|
27
|
+
export type { TransportCoherenceOptions } from "./transport-coherence.js";
|
|
28
|
+
export type { ProbeVolumeOptions } from "./probe-volume.js";
|
|
29
|
+
export type { IdEnumerationOptions } from "./id-enumeration.js";
|
|
21
30
|
export { sessionIntegrityDetector } from "./session-integrity.js";
|
|
22
31
|
export type { SessionIntegrityOptions } from "./session-integrity.js";
|
|
23
32
|
export { identityRotationDetector } from "./identity-rotation.js";
|
|
@@ -53,4 +62,6 @@ export type { BotSignature, BotCategory, Verification } from "./known-bots.js";
|
|
|
53
62
|
* `clearanceDetector` is not here either, because it needs the challenge service —
|
|
54
63
|
* the engine adds it automatically once `challenge.secrets` is configured.
|
|
55
64
|
*/
|
|
56
|
-
export declare function defaultDetectors(
|
|
65
|
+
export declare function defaultDetectors(options?: {
|
|
66
|
+
crawlerVerification?: CrawlerVerificationOptions;
|
|
67
|
+
}): Detector[];
|
|
@@ -16,7 +16,46 @@ import { MultiPatternMatcher } from "../internal/matcher.js";
|
|
|
16
16
|
*/
|
|
17
17
|
export type BotCategory = "search" | "ai" | "seo" | "social" | "monitoring" | "archive" | "feed" | "security" | "advertising" | "library" | "headless"
|
|
18
18
|
/** A real browser engine embedded in a desktop application, with a person driving it. */
|
|
19
|
-
| "embedded"
|
|
19
|
+
| "embedded"
|
|
20
|
+
/**
|
|
21
|
+
* Price, stock and catalogue collection: comparison shopping, marketplace feeds,
|
|
22
|
+
* repricing tools.
|
|
23
|
+
*
|
|
24
|
+
* Its own category because it is the one kind of crawling a shop has a commercial
|
|
25
|
+
* opinion about rather than a technical one. It is not `seo` — nothing here is
|
|
26
|
+
* auditing your site for you — and it is not `scraper`, which is a *behavioural*
|
|
27
|
+
* verdict this library reaches on its own. This is a client that says what it is.
|
|
28
|
+
*/
|
|
29
|
+
| "commerce"
|
|
30
|
+
/**
|
|
31
|
+
* Accessibility auditing: contrast, landmarks, ARIA, WCAG conformance.
|
|
32
|
+
*
|
|
33
|
+
* Separated from `monitoring` because the answer is almost always different. A site
|
|
34
|
+
* owner who blocks uptime probes still wants the tool their accessibility team runs
|
|
35
|
+
* to reach the page, and frequently does not know it is arriving as a bot at all.
|
|
36
|
+
*/
|
|
37
|
+
| "accessibility"
|
|
38
|
+
/**
|
|
39
|
+
* A mail or messaging gateway checking a link on somebody's behalf.
|
|
40
|
+
*
|
|
41
|
+
* Its own category because of who pays when it is blocked. A social preview that fails
|
|
42
|
+
* costs a card; one of these failing tells a real person, in their inbox, that the link
|
|
43
|
+
* they were sent could not be verified — and they were never the one crawling. They also
|
|
44
|
+
* arrive with none of a browser's marks: from a datacentre, once, with no cookie and no
|
|
45
|
+
* referer, moments after a message was delivered, which is a shape that reads as
|
|
46
|
+
* automation because it *is* automation, acting for a human.
|
|
47
|
+
*/
|
|
48
|
+
| "email-security"
|
|
49
|
+
/**
|
|
50
|
+
* Research and measurement: universities, internet-measurement projects, plagiarism
|
|
51
|
+
* and citation indexes.
|
|
52
|
+
*
|
|
53
|
+
* Distinct from `ai` on purpose. Both read the whole page and neither sends a person,
|
|
54
|
+
* but the decision differs: an operator refusing to feed a commercial model may be
|
|
55
|
+
* perfectly happy to appear in a citation index, and folding the two together forces
|
|
56
|
+
* one answer onto two questions.
|
|
57
|
+
*/
|
|
58
|
+
| "academic" | "other";
|
|
20
59
|
/** Every category, for anything that has to enumerate them — a rule editor, a report. */
|
|
21
60
|
export declare const BOT_CATEGORIES: readonly BotCategory[];
|
|
22
61
|
/**
|
|
@@ -40,6 +79,26 @@ export type Verification = {
|
|
|
40
79
|
} | {
|
|
41
80
|
kind: "ip-ranges";
|
|
42
81
|
publishedAt?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The operator publishes a proof this library cannot check by itself, and you can.
|
|
85
|
+
*
|
|
86
|
+
* A signed request under [Web Bot Auth](https://www.rfc-editor.org/rfc/rfc9421), a
|
|
87
|
+
* CDN that has already verified the crawler and says so in a header it adds, an ASN
|
|
88
|
+
* lookup against data you hold — all of them are conclusive, and none of them are
|
|
89
|
+
* something a detection library should be doing on its own: two need a network
|
|
90
|
+
* dependency and the third needs a key it has no business fetching.
|
|
91
|
+
*
|
|
92
|
+
* So the claim is marked verifiable-by-you, and stays *unverified* until you supply a
|
|
93
|
+
* verifier for it in `crawlerVerification.verifiers`. Marked and unsupplied behaves
|
|
94
|
+
* exactly like `none`: neither confirmed nor accused.
|
|
95
|
+
*
|
|
96
|
+
* `via` names the mechanism, for the operator reading this table to know what they
|
|
97
|
+
* would have to write.
|
|
98
|
+
*/
|
|
99
|
+
| {
|
|
100
|
+
kind: "proof";
|
|
101
|
+
via: string;
|
|
43
102
|
} | {
|
|
44
103
|
kind: "none";
|
|
45
104
|
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Detector } from "./types.js";
|
|
2
|
+
export interface ParameterSweepOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Distinct path-and-query combinations at or above which a sweep is worth reporting.
|
|
5
|
+
* Default 25. Cannot exceed {@link MAX_TRACKED_QUERIES}, where the count saturates;
|
|
6
|
+
* asking for more throws rather than never firing.
|
|
7
|
+
*/
|
|
8
|
+
threshold?: number;
|
|
9
|
+
/**
|
|
10
|
+
* How many variants must sit on one path before this is a sweep rather than browsing.
|
|
11
|
+
* Default 8 — that is, twenty-five variants across three paths reports, and
|
|
12
|
+
* twenty-five variants across twenty paths does not.
|
|
13
|
+
*/
|
|
14
|
+
variantsPerPath?: number;
|
|
15
|
+
/** Minimum requests before the shape means anything. Default 20. */
|
|
16
|
+
minRequests?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The scraping that `crawl-breadth` cannot see.
|
|
20
|
+
*
|
|
21
|
+
* Breadth counts distinct *paths*, and a path has no query string on it. So the shape
|
|
22
|
+
* it reads as "somebody rereading one page" is also the shape of enumerating a
|
|
23
|
+
* catalogue: `/products?page=1` through `?page=200` is one path and two hundred
|
|
24
|
+
* requests. Measured, on the same two hundred requests expressed both ways — as
|
|
25
|
+
* distinct paths it scored 62 and was called `suspected-bot`; as `?page=N` it scored 55
|
|
26
|
+
* and passed as `unknown`. Paginated collection is not an exotic case, it is how
|
|
27
|
+
* catalogues, search results and APIs are actually taken.
|
|
28
|
+
*
|
|
29
|
+
* So this counts the other thing: distinct parameterisations, and how many of them
|
|
30
|
+
* stack onto a single path. Both halves are needed. A high variant count alone is
|
|
31
|
+
* ordinary — a shop's own visitors filter and sort — and it is the *concentration* that
|
|
32
|
+
* separates a person changing their mind from a machine walking an index.
|
|
33
|
+
*
|
|
34
|
+
* `weak`, and deliberately. A person paging through search results produces a smaller
|
|
35
|
+
* version of exactly this, and someone with a slow connection retrying looks similar
|
|
36
|
+
* again. It is a shape, not a motive; its value is as a second signal beside an actor
|
|
37
|
+
* that has already failed something sharper.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parameterSweepDetector(options?: ParameterSweepOptions): Detector;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Detector } from "./types.js";
|
|
2
|
+
export interface ProbeVolumeOptions {
|
|
3
|
+
/** Reported responses before the ratio means anything. Default 20. */
|
|
4
|
+
minResponses?: number;
|
|
5
|
+
/** Share of them that must be misses. Default 0.8. */
|
|
6
|
+
missRatio?: number;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* An actor that is looking for something rather than reading anything.
|
|
10
|
+
*
|
|
11
|
+
* The oldest tell there is for a scanner, and the one this library could not see: it
|
|
12
|
+
* decides *before* the response exists, which is what lets it shape the response and also
|
|
13
|
+
* what hides the status code from it. Fed back through `recordOutcome`, the shape is
|
|
14
|
+
* unmistakable — a person browsing a site does not generate forty misses in a row, and a
|
|
15
|
+
* wordlist does almost nothing else.
|
|
16
|
+
*
|
|
17
|
+
* Counts 404 and 410 only. A 403 is usually this library's own doing, and counting it
|
|
18
|
+
* would let a rule that challenges an actor manufacture the evidence for challenging it.
|
|
19
|
+
* A 500 is the site's problem and says nothing about the client.
|
|
20
|
+
*
|
|
21
|
+
* `moderate`, not higher. A site that has just moved its URLs produces this from perfectly
|
|
22
|
+
* ordinary readers, and so does a feed reader working through a list of removed articles.
|
|
23
|
+
* It is also entirely absent unless the application reports outcomes, which is why nothing
|
|
24
|
+
* else depends on it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function probeVolumeDetector(options?: ProbeVolumeOptions): Detector;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Detector } from "./types.js";
|
|
2
|
+
export interface TransportCoherenceOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Report a claimed browser arriving over HTTP/1.0. Default true.
|
|
5
|
+
*
|
|
6
|
+
* Turn it off if something in front of this application speaks HTTP/1.0 to it. A few
|
|
7
|
+
* older load balancers and reverse proxies still do, and where that is true every
|
|
8
|
+
* request arrives that way — so the signal says something about your infrastructure
|
|
9
|
+
* rather than about your visitors, and a detector that fires on all of them is worse
|
|
10
|
+
* than one that fires on none.
|
|
11
|
+
*/
|
|
12
|
+
legacyHttp?: boolean;
|
|
13
|
+
/** Requests an actor must have made before an all-HEAD visit means anything. Default 8. */
|
|
14
|
+
minHeadRequests?: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* How a claimed browser *moves*, rather than what it says.
|
|
18
|
+
*
|
|
19
|
+
* The header checks read one request against the client it claims to be. This reads the
|
|
20
|
+
* transport underneath and the verbs across a visit, which are harder to copy because
|
|
21
|
+
* they are not in the part of the request most tools let you set.
|
|
22
|
+
*
|
|
23
|
+
* Two things, both measured as blind spots before this existed — a client claiming
|
|
24
|
+
* Chrome 120 over HTTP/1.0, and one whose entire visit is HEAD, each scored exactly what
|
|
25
|
+
* the honest control scored.
|
|
26
|
+
*
|
|
27
|
+
* Neither goes above `moderate`, and the reasons are different. HTTP/1.0 can be an
|
|
28
|
+
* intermediary's doing rather than the client's. An all-HEAD visit is a strong shape but a
|
|
29
|
+
* link checker is a real and mostly harmless thing to be.
|
|
30
|
+
*/
|
|
31
|
+
export declare function transportCoherenceDetector(options?: TransportCoherenceOptions): Detector;
|