@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/corpus/schema.d.ts
CHANGED
|
@@ -61,6 +61,14 @@ export interface CaseRequest {
|
|
|
61
61
|
tlsFingerprint?: string;
|
|
62
62
|
/** Milliseconds after the case's start time. Drives the behavioural detectors. */
|
|
63
63
|
atMs?: number;
|
|
64
|
+
/**
|
|
65
|
+
* What the application answered, if the case is about that.
|
|
66
|
+
*
|
|
67
|
+
* The engine decides before a response exists, so this is reported back afterwards the
|
|
68
|
+
* way an adapter reports it. Only cases about response shape need it — a scan that is
|
|
69
|
+
* almost all misses being the one that matters.
|
|
70
|
+
*/
|
|
71
|
+
status?: number;
|
|
64
72
|
/** The source could not supply the full header set. See `RequestFacts.partialHeaders`. */
|
|
65
73
|
partialHeaders?: boolean;
|
|
66
74
|
}
|
package/dist/crawler-ranges.d.ts
CHANGED
|
@@ -81,6 +81,37 @@ export interface RefreshResult {
|
|
|
81
81
|
* an error rather than a guess.
|
|
82
82
|
*/
|
|
83
83
|
export declare function fetchCrawlerRanges(source: PublishedRangeSource, options?: RefreshOptions): Promise<string[]>;
|
|
84
|
+
/** Where a reputation or hosting-provider address list is published. */
|
|
85
|
+
export interface AddressListSource {
|
|
86
|
+
/** The range set to install it as. `denylist` blocks; `datacenter` corroborates. */
|
|
87
|
+
id: "denylist" | "datacenter" | "allowlist";
|
|
88
|
+
url: string;
|
|
89
|
+
}
|
|
90
|
+
export interface AddressListOptions extends RefreshOptions {
|
|
91
|
+
/** Entries to accept before refusing the list. Default 100,000. */
|
|
92
|
+
maxPrefixes?: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fetches a published address list — a reputation feed, a hosting provider's own ranges.
|
|
96
|
+
*
|
|
97
|
+
* The same hardened path as {@link fetchCrawlerRanges}: HTTPS only, a size cap, comments
|
|
98
|
+
* stripped, JSON `prefixes` documents or one prefix per line, and a list that is empty,
|
|
99
|
+
* oversized or contains a block big enough to matter is refused **whole** rather than in
|
|
100
|
+
* part. It is a separate entry point because the limits differ — a reputation feed is
|
|
101
|
+
* thousands of entries where a crawler's is hundreds, and calling one a crawler's list in
|
|
102
|
+
* an error message helps nobody.
|
|
103
|
+
*
|
|
104
|
+
* It does not install anything. Hand the result to `updateRanges("denylist", …)` when you
|
|
105
|
+
* are ready, which is the same two-step the crawler path takes and for the same reason:
|
|
106
|
+
* fetching is the part that can fail, and installing is the part that changes behaviour.
|
|
107
|
+
*
|
|
108
|
+
* **A denylist entry is `certain`.** It does not corroborate anything — it decides, and it
|
|
109
|
+
* blocks people. A feed is somebody else's judgement about an address, refreshed on
|
|
110
|
+
* somebody else's schedule, and an address that was a bot last month may be a customer's
|
|
111
|
+
* home connection this month. Read what you are subscribing to, and prefer `datacenter`
|
|
112
|
+
* for anything you have not decided about yourself.
|
|
113
|
+
*/
|
|
114
|
+
export declare function fetchAddressList(source: AddressListSource, options?: AddressListOptions): Promise<string[]>;
|
|
84
115
|
/**
|
|
85
116
|
* Fetches every published list and installs it.
|
|
86
117
|
*
|
|
@@ -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;
|
|
@@ -18,7 +18,15 @@ export type { RateAnomalyOptions } from "./rate-anomaly.js";
|
|
|
18
18
|
export { cadenceDetector } from "./cadence.js";
|
|
19
19
|
export type { CadenceOptions } from "./cadence.js";
|
|
20
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";
|
|
21
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";
|
|
22
30
|
export { sessionIntegrityDetector } from "./session-integrity.js";
|
|
23
31
|
export type { SessionIntegrityOptions } from "./session-integrity.js";
|
|
24
32
|
export { identityRotationDetector } from "./identity-rotation.js";
|
|
@@ -35,6 +35,17 @@ export type BotCategory = "search" | "ai" | "seo" | "social" | "monitoring" | "a
|
|
|
35
35
|
* to reach the page, and frequently does not know it is arriving as a bot at all.
|
|
36
36
|
*/
|
|
37
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"
|
|
38
49
|
/**
|
|
39
50
|
* Research and measurement: universities, internet-measurement projects, plagiarism
|
|
40
51
|
* and citation indexes.
|
|
@@ -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;
|
package/dist/element/index.cjs
CHANGED
|
@@ -3878,7 +3878,10 @@ button[disabled] { opacity: .5; cursor: default; }
|
|
|
3878
3878
|
.tab[aria-selected="true"] { color: var(--ink); border-bottom-color: var(--s1); }
|
|
3879
3879
|
|
|
3880
3880
|
/* --- layout ------------------------------------------------------------- */
|
|
3881
|
-
|
|
3881
|
+
/* The top padding is the gap under the sticky header. At 18px the counter row sat almost
|
|
3882
|
+
against the header's border and read as part of it; the tiles carry their own border, so
|
|
3883
|
+
two lines were meeting with nothing between them. */
|
|
3884
|
+
main { padding: 28px 20px 64px; max-width: 1680px; margin: 0 auto; }
|
|
3882
3885
|
.stack { display: grid; gap: 16px; }
|
|
3883
3886
|
/* Everything above the feed is drawn by script once the first snapshot arrives, which
|
|
3884
3887
|
inserts a block of content above what is already laid out. The browser's scroll
|