@osqd/bothandlerjs 0.3.0 → 0.5.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/CHANGELOG.md +157 -1
- package/README.md +1 -1
- package/dist/challenge/index.d.ts +18 -0
- package/dist/challenge/interaction.d.ts +215 -0
- package/dist/challenge/page.d.ts +18 -0
- package/dist/cli.cjs +829 -71
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +829 -71
- 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 +20 -0
- package/dist/corpus/index.cjs.map +1 -1
- package/dist/corpus/index.js +20 -0
- package/dist/corpus/index.js.map +1 -1
- package/dist/dashboard/client/actions.d.ts +17 -0
- package/dist/dashboard/client/actor.d.ts +5 -0
- package/dist/dashboard/client/api.d.ts +15 -0
- package/dist/dashboard/client/app.d.ts +37 -0
- package/dist/dashboard/client/bars.d.ts +4 -0
- package/dist/dashboard/client/boot.d.ts +5 -0
- package/dist/dashboard/client/charts.d.ts +25 -0
- package/dist/dashboard/client/css.d.ts +11 -0
- package/dist/dashboard/client/dom.d.ts +55 -0
- package/dist/dashboard/client/draft.d.ts +46 -0
- package/dist/dashboard/client/feed.d.ts +22 -0
- package/dist/dashboard/client/format.d.ts +37 -0
- package/dist/dashboard/client/guard.d.ts +3 -0
- package/dist/dashboard/client/index.d.ts +1 -0
- package/dist/dashboard/client/outcome.d.ts +33 -0
- package/dist/dashboard/client/pager.d.ts +32 -0
- package/dist/dashboard/client/panels.d.ts +40 -0
- package/dist/dashboard/client/policy.d.ts +25 -0
- package/dist/dashboard/client/query.d.ts +42 -0
- package/dist/dashboard/client/ranges.d.ts +2 -0
- package/dist/dashboard/client/registry.d.ts +4 -0
- package/dist/dashboard/client/replay.d.ts +14 -0
- package/dist/dashboard/client/result.d.ts +10 -0
- package/dist/dashboard/client/store.d.ts +128 -0
- package/dist/dashboard/client/stream.d.ts +14 -0
- package/dist/dashboard/client/tester.d.ts +1 -0
- package/dist/dashboard/client/types.d.ts +69 -0
- package/dist/dashboard/client.generated.d.ts +1 -1
- package/dist/dashboard/page.d.ts +25 -0
- package/dist/dashboard/sections.d.ts +92 -0
- package/dist/dashboard/types.d.ts +2 -82
- package/dist/detectors/crawler-verification.d.ts +34 -1
- package/dist/detectors/index.d.ts +4 -1
- package/dist/detectors/known-bots.d.ts +49 -1
- package/dist/element/config.d.ts +135 -0
- package/dist/element/index.cjs +5150 -0
- package/dist/element/index.cjs.map +1 -0
- package/dist/element/index.d.ts +137 -0
- package/dist/element/index.js +5131 -0
- package/dist/element/index.js.map +1 -0
- package/dist/index.cjs +820 -71
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +811 -71
- package/dist/index.js.map +1 -1
- package/dist/metrics.d.ts +27 -0
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/presets.d.ts +47 -0
- package/dist/state.d.ts +1 -1
- package/docs/challenge/index.md +1 -0
- package/docs/challenge/interaction.md +325 -0
- package/docs/course/11-the-challenge.md +36 -1
- package/docs/course/13-operating-it.md +20 -2
- package/docs/detection/signatures.md +36 -0
- package/docs/index.md +2 -0
- package/docs/operations/dashboard.md +28 -0
- package/docs/operations/embedding.md +348 -0
- package/docs/operations/index.md +1 -0
- package/docs/policy/presets.md +53 -0
- package/docs/start/choosing-a-policy.md +1 -0
- package/package.json +8 -3
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The three things you can do to one client, rather than to a class of request.
|
|
3
|
+
*
|
|
4
|
+
* Shared by the actor drill-down and the Actors screen, which offer the same three and
|
|
5
|
+
* would otherwise offer them slightly differently.
|
|
6
|
+
*
|
|
7
|
+
* Allowlisting is the consequential one and it is the one that asks twice. **An
|
|
8
|
+
* allowlisted address is not judged leniently; it is not judged at all** — detection
|
|
9
|
+
* does not run, no evidence is produced, no rule sees it — so the button states the
|
|
10
|
+
* address it is about to exempt and waits for a second click. That is deliberately not
|
|
11
|
+
* a dialog: a confirmation you can dismiss without reading is a click with extra steps,
|
|
12
|
+
* whereas a button that changes into the sentence it is about to enact has to be read
|
|
13
|
+
* to be pressed.
|
|
14
|
+
*/
|
|
15
|
+
export type AfterAction = () => void;
|
|
16
|
+
export declare function isConfirming(): boolean;
|
|
17
|
+
export declare function actorActions(key: string, after: AfterAction): HTMLElement[];
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Wires the panel's own Close button. Called once, and only when the panel exists. */
|
|
2
|
+
export declare function initActor(): void;
|
|
3
|
+
/** Opens the drill-down above the feed and scrolls it into view. */
|
|
4
|
+
export declare function openActor(key: string): void;
|
|
5
|
+
export declare function drawActor(): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page's two ways of talking to its server.
|
|
3
|
+
*
|
|
4
|
+
* Every write is JSON with a JSON content type, and that is not a style choice: the
|
|
5
|
+
* server refuses a body it did not get as `application/json` precisely because an HTML
|
|
6
|
+
* form cannot send one, which is what stops somebody else's page from posting here
|
|
7
|
+
* with an operator's credentials attached.
|
|
8
|
+
*/
|
|
9
|
+
export declare function getJson<T>(path: string): Promise<T>;
|
|
10
|
+
export interface PostResult<T> {
|
|
11
|
+
ok: boolean;
|
|
12
|
+
data: T;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function postJson<T>(path: string, body: unknown): Promise<PostResult<T>>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { TabName } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The handful of things every module needs to be able to do to the page as a whole.
|
|
4
|
+
*
|
|
5
|
+
* A mutable record filled in by `index.ts` at start-up, rather than each module
|
|
6
|
+
* importing the module that owns `draw`. The feed opens the actor panel, the actor
|
|
7
|
+
* panel drafts a rule into the policy editor, the policy editor switches to its own
|
|
8
|
+
* tab: written as direct imports that is a cycle, and a cycle in a bundle is a
|
|
9
|
+
* half-initialised module waiting to be discovered at run time. One indirection costs
|
|
10
|
+
* a property lookup and makes the shape obvious.
|
|
11
|
+
*/
|
|
12
|
+
export interface App {
|
|
13
|
+
/** Redraws the active view on the next frame. Coalesced; safe to call per event. */
|
|
14
|
+
draw: () => void;
|
|
15
|
+
/** Redraws immediately, for the cases where a click has to be reflected before the next frame. */
|
|
16
|
+
drawNow: () => void;
|
|
17
|
+
showTab: (tab: TabName, options?: {
|
|
18
|
+
focus?: boolean;
|
|
19
|
+
replace?: boolean;
|
|
20
|
+
push?: boolean;
|
|
21
|
+
}) => void;
|
|
22
|
+
/** Puts the feed's filter state into the URL, so a view can be sent to somebody. */
|
|
23
|
+
syncUrl: () => void;
|
|
24
|
+
}
|
|
25
|
+
export declare const app: App;
|
|
26
|
+
/**
|
|
27
|
+
* A transient message in the corner.
|
|
28
|
+
*
|
|
29
|
+
* Lives here rather than in the policy editor because the guard form, the feed's
|
|
30
|
+
* exporter and the stream all raise them now, and three copies of a six-line function
|
|
31
|
+
* is how they end up looking different from each other.
|
|
32
|
+
*/
|
|
33
|
+
export declare function toast(kind: "ok" | "bad" | "warn", title: string, detail?: string): void;
|
|
34
|
+
/** Hands the viewer a file. Used by the settings export and the feed export. */
|
|
35
|
+
export declare function download(text: string, filename: string, type: string): void;
|
|
36
|
+
/** Today, as a filename fragment. Every export the page produces is stamped with it. */
|
|
37
|
+
export declare function today(): string;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** A horizontal bar list, sorted by value, capped at what a panel can show without scrolling. */
|
|
2
|
+
export declare function drawBars(target: HTMLElement, rows: Iterable<[string, number]>, emptyText: string): void;
|
|
3
|
+
/** A record of counters as bar rows. */
|
|
4
|
+
export declare function pairs(record: Record<string, number> | undefined): Array<[string, number]>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stacked bars, two series, plus a marker row for denials.
|
|
3
|
+
*
|
|
4
|
+
* Two series rather than three because served/mitigated/denied as three fills would
|
|
5
|
+
* need a third hue that survives colour-vision simulation beside these two, and none
|
|
6
|
+
* does; a denial is a status rather than a series, so it gets a status marker and a
|
|
7
|
+
* label instead.
|
|
8
|
+
*/
|
|
9
|
+
export declare function drawTraffic(): void;
|
|
10
|
+
/**
|
|
11
|
+
* The score distribution: how close ordinary traffic runs to the line.
|
|
12
|
+
*
|
|
13
|
+
* Two populations, and the panel now says which one it is drawing rather than leaving
|
|
14
|
+
* the reader to assume. **Since start** comes from the same counters as the Prometheus
|
|
15
|
+
* endpoint, so it covers the whole run and agrees with whatever your alerting says.
|
|
16
|
+
* **This window** counts the few hundred requests the page is still holding, which is
|
|
17
|
+
* the right answer to "what is happening right now" and the wrong answer to "where
|
|
18
|
+
* should the threshold be" — and it was the only answer available before, sitting
|
|
19
|
+
* beside panels counting since start under an identical grey subtitle.
|
|
20
|
+
*
|
|
21
|
+
* Proven requests are excluded from both rather than piled into the last bucket: their
|
|
22
|
+
* score is 100 by definition and plays no part in any decision.
|
|
23
|
+
*/
|
|
24
|
+
export declare function drawScores(): void;
|
|
25
|
+
export declare function drawLatency(): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector escaping, on its own so both halves can have it.
|
|
3
|
+
*
|
|
4
|
+
* `dom.ts` is the browser client's entry into the document and carries module state that
|
|
5
|
+
* has to be initialised in order; the embeddable element imports it lazily for exactly
|
|
6
|
+
* that reason. But the element also assembles selectors from developer-supplied panel ids
|
|
7
|
+
* *synchronously*, before any of that has happened — so the escape lives here, in a leaf
|
|
8
|
+
* with nothing behind it, rather than being duplicated or dragging the client graph in
|
|
9
|
+
* early.
|
|
10
|
+
*/
|
|
11
|
+
export declare function cssEscape(value: string): string;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page's only way of putting something in the document.
|
|
3
|
+
*
|
|
4
|
+
* `textContent`, always. Every string that reaches this file — a User-Agent, a path, a
|
|
5
|
+
* detector's summary, a rule id somebody typed into the editor — was written by
|
|
6
|
+
* somebody else, and one `innerHTML` anywhere below turns a bot's User-Agent into
|
|
7
|
+
* script running in an operator's browser. A test asserts the served page contains
|
|
8
|
+
* none of the four ways to do that, and `scripts/build-client.mjs` refuses to bundle
|
|
9
|
+
* one.
|
|
10
|
+
*/
|
|
11
|
+
import { cssEscape } from "./css.js";
|
|
12
|
+
export declare function el<K extends keyof HTMLElementTagNameMap>(tag: K, className?: string | null, value?: string | number | null): HTMLElementTagNameMap[K];
|
|
13
|
+
export declare function svgEl(name: string, attributes?: Record<string, string | number>): SVGElement;
|
|
14
|
+
export declare function svgText(attributes: Record<string, string | number>, text: string | number): SVGElement;
|
|
15
|
+
export declare function clear(node: Node): void;
|
|
16
|
+
/**
|
|
17
|
+
* Ids are base64url today, which needs no escaping — but a selector assembled from a
|
|
18
|
+
* value is only safe until somebody widens the alphabet, and `CSS.escape` costs
|
|
19
|
+
* nothing. It lives in its own module because the embeddable element needs it without
|
|
20
|
+
* loading this one; re-exported here so the rest of the client reads unchanged.
|
|
21
|
+
*/
|
|
22
|
+
export { cssEscape };
|
|
23
|
+
/** Points the client at a shadow root. Call it before the rest of the client loads. */
|
|
24
|
+
export declare function setRoot(node: Document | ShadowRoot | HTMLElement, host?: HTMLElement): void;
|
|
25
|
+
/** Updates the element the theme hangs off, without disturbing the rest. */
|
|
26
|
+
export declare function setThemeHost(host: HTMLElement): void;
|
|
27
|
+
export declare function isEmbedded(): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* What global-ish events are listened on.
|
|
30
|
+
*
|
|
31
|
+
* The document when this owns the page. The dashboard's own subtree when it does not, so
|
|
32
|
+
* a digit pressed on the host's page is the host's business and not a tab change here.
|
|
33
|
+
*/
|
|
34
|
+
export declare function eventTarget(): EventTarget;
|
|
35
|
+
/** The node every lookup runs against. */
|
|
36
|
+
export declare function rootNode(): Document | ShadowRoot;
|
|
37
|
+
/** The element carrying `data-theme` and the custom properties. */
|
|
38
|
+
export declare function themeElement(): HTMLElement;
|
|
39
|
+
export declare function $(id: string): HTMLElement;
|
|
40
|
+
export declare function byId<T extends HTMLElement>(id: string): T;
|
|
41
|
+
/** A CSS custom property's value, for the charts — which draw in the theme's own colours. */
|
|
42
|
+
export declare function css(name: string): string;
|
|
43
|
+
/**
|
|
44
|
+
* Ties a `<label>` to the control it names.
|
|
45
|
+
*
|
|
46
|
+
* A label sitting next to an input is a label to a reader who can see the layout and
|
|
47
|
+
* nothing at all to a screen reader: it announces "edit, blank" and moves on. Both form
|
|
48
|
+
* builders on this page had that defect, and both were caught by the automated
|
|
49
|
+
* accessibility pass rather than by anybody looking — which is the argument for having
|
|
50
|
+
* one.
|
|
51
|
+
*
|
|
52
|
+
* Only for the controls a label can point at. A group of chips or a segmented control is
|
|
53
|
+
* several buttons, and those carry their own names.
|
|
54
|
+
*/
|
|
55
|
+
export declare function label(text: string, control: HTMLElement | HTMLElement[]): HTMLLabelElement;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DashboardEntry } from "./types.js";
|
|
2
|
+
/** A rule as the editor holds it: plain JSON, plus the card's open/closed state. */
|
|
3
|
+
export interface EditorRule {
|
|
4
|
+
id: string;
|
|
5
|
+
match: Record<string, unknown>;
|
|
6
|
+
action: string;
|
|
7
|
+
params?: Record<string, unknown>;
|
|
8
|
+
reason?: string;
|
|
9
|
+
/** Editor-only: whether this card is expanded. Stripped before anything is submitted. */
|
|
10
|
+
_open?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface Draft {
|
|
13
|
+
rule: EditorRule;
|
|
14
|
+
/** One line saying what the draft matched on, and why that and not something else. */
|
|
15
|
+
because: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Turns a request you are looking at into a rule you can preview.
|
|
19
|
+
*
|
|
20
|
+
* This is the half of the loop the dashboard was missing. The row detail could already
|
|
21
|
+
* turn a request into a replay line and a corpus case — the offline loop, where a
|
|
22
|
+
* verdict you disagree with becomes a test. The online loop ran the other way and had
|
|
23
|
+
* no help at all: you saw an actor worth acting on, then went and hand-wrote a rule in
|
|
24
|
+
* a different tab, guessing at which field would catch it.
|
|
25
|
+
*
|
|
26
|
+
* Three decisions in here, and all three are about not being clever on somebody's
|
|
27
|
+
* behalf:
|
|
28
|
+
*
|
|
29
|
+
* **The action is always `tag`.** Never `block`, never `challenge`, whatever the
|
|
30
|
+
* request looks like. A drafted rule is a starting point that has not been reviewed by
|
|
31
|
+
* anyone, and the dashboard picking a terminal action for a request that annoyed you is
|
|
32
|
+
* exactly the reflex this library exists to interrupt. The operator picks the action;
|
|
33
|
+
* the guard still checks it afterwards either way.
|
|
34
|
+
*
|
|
35
|
+
* **It matches on the strongest thing the request actually proves**, in a fixed order:
|
|
36
|
+
* a claimed-and-checked identity, else the detectors whose evidence was proven, else
|
|
37
|
+
* the verdict with a score floor. Each of those is a fact about the client. A path
|
|
38
|
+
* would narrow it further and is deliberately left out — the request happens to have
|
|
39
|
+
* one, which is not the same as the rule being about it.
|
|
40
|
+
*
|
|
41
|
+
* **It goes last.** Appending is the only position that cannot change what any
|
|
42
|
+
* existing rule does, because first match wins. A rule that never fires shows up as
|
|
43
|
+
* "never matched" in the preview, which is a better way to learn it is shadowed than
|
|
44
|
+
* discovering it silently took over from something else.
|
|
45
|
+
*/
|
|
46
|
+
export declare function draftRule(entry: DashboardEntry, existingIds?: readonly string[]): Draft;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export declare function initFeed(): void;
|
|
2
|
+
/** Applies filter state that arrived in the URL rather than from a click. */
|
|
3
|
+
export declare function reflectFilterButtons(): void;
|
|
4
|
+
/**
|
|
5
|
+
* Fetches the entries this viewer never received, and merges them.
|
|
6
|
+
*
|
|
7
|
+
* They were never lost. The rate cap keeps a burst off the *stream* and the lag guard
|
|
8
|
+
* drops frames a slow socket cannot take, but both leave the ring alone — and the opening
|
|
9
|
+
* replay is droppable too, so a first load of a busy dashboard can arrive with most of the
|
|
10
|
+
* backlog missing. `/api/feed` serves that ring whole, and `ingest` is keyed on the request
|
|
11
|
+
* id, so merging it is idempotent: what is already held is refreshed, what is missing is
|
|
12
|
+
* added.
|
|
13
|
+
*
|
|
14
|
+
* On demand rather than on a timer. Skips happen exactly when the origin is busiest, and a
|
|
15
|
+
* dashboard that answered every skip by re-fetching the whole ring would be a load
|
|
16
|
+
* amplifier pointed at the process it is meant to be watching — which is the thing
|
|
17
|
+
* `maxEventsPerSecond` exists to prevent.
|
|
18
|
+
*/
|
|
19
|
+
export declare function loadSkipped(): Promise<void>;
|
|
20
|
+
export declare function drawFeed(): void;
|
|
21
|
+
/** Drops every cached node. Used when the feed is cleared under the page's feet. */
|
|
22
|
+
export declare function resetFeedCache(): void;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Number, duration and window formatting. Pure, so the awkward cases can be tested. */
|
|
2
|
+
export declare function n(value: number | undefined): string;
|
|
3
|
+
export declare function pct(part: number, whole: number): string;
|
|
4
|
+
export declare function ms(value: number): string;
|
|
5
|
+
export declare function uptime(milliseconds: number): string;
|
|
6
|
+
export declare function rangeLabel(milliseconds: number): string;
|
|
7
|
+
/** Wall-clock time for a feed row. Local, seconds included, because a feed moves in seconds. */
|
|
8
|
+
/**
|
|
9
|
+
* A time, always as 24-hour HH:MM:SS.
|
|
10
|
+
*
|
|
11
|
+
* Built from the local components rather than handed to `toLocaleTimeString`, which
|
|
12
|
+
* answers in whatever the viewer's locale prefers — so the same feed read "4:40:46 PM" on
|
|
13
|
+
* one operator's screen and "16:40:46" on the next, and a dashboard two people look at
|
|
14
|
+
* together should not disagree with itself about what time it is. Local time, not UTC:
|
|
15
|
+
* this is the clock on the wall next to the server somebody is watching.
|
|
16
|
+
*/
|
|
17
|
+
export declare function clockTime(at: number): string;
|
|
18
|
+
/** A date, always as DD-MM-YYYY. */
|
|
19
|
+
export declare function clockDate(at: number): string;
|
|
20
|
+
/**
|
|
21
|
+
* Both, for the places where the time alone is ambiguous.
|
|
22
|
+
*
|
|
23
|
+
* An actor's first sighting can be days back, and "first seen 09:14:02" invites the reader
|
|
24
|
+
* to assume it was this morning.
|
|
25
|
+
*/
|
|
26
|
+
export declare function clockStamp(at: number): string;
|
|
27
|
+
/**
|
|
28
|
+
* What "this window" actually holds, said out loud.
|
|
29
|
+
*
|
|
30
|
+
* Half the panels on the Statistics screen count the retained ring and half count
|
|
31
|
+
* since the process started, and until now both wore the same quiet grey subtitle.
|
|
32
|
+
* They are different populations — on a busy server the ring can be ninety seconds of
|
|
33
|
+
* a three-week run — so comparing a panel from one against a panel from the other is a
|
|
34
|
+
* mistake the page was inviting. Every "this window" label now says how much window
|
|
35
|
+
* there is.
|
|
36
|
+
*/
|
|
37
|
+
export declare function windowLabel(count: number, oldestAt: number | undefined, now: number): string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DashboardEntry } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* One request has one of four outcomes, and every part of the page agrees on which.
|
|
4
|
+
*
|
|
5
|
+
* Served, mitigated (something happened that a legitimate client can still get past),
|
|
6
|
+
* denied, or pending — assessed, with no decision recorded, which is what `assess()`
|
|
7
|
+
* on its own produces and what a row looks like for the moment between the two events.
|
|
8
|
+
*
|
|
9
|
+
* Pure, and exported on its own, because the table, the left edges, the timeline, the
|
|
10
|
+
* actor mix and the statistics panels all classify the same way. Two of them disagreeing
|
|
11
|
+
* about what a denial is would be a bug nobody could see.
|
|
12
|
+
*/
|
|
13
|
+
export type Outcome = "allow" | "mitigate" | "deny" | "pending";
|
|
14
|
+
export declare function outcome(entry: Pick<DashboardEntry, "action">): Outcome;
|
|
15
|
+
/** The same classification for a bare action name, which is what the rule editor has. */
|
|
16
|
+
export declare function actionKind(action: string | undefined): Outcome;
|
|
17
|
+
/** The verdict badge: its class, and the word on it. */
|
|
18
|
+
export declare function verdictBadge(entry: Pick<DashboardEntry, "verdict" | "bypass">): [className: string, label: string];
|
|
19
|
+
/**
|
|
20
|
+
* How many assessments proved a *bot*.
|
|
21
|
+
*
|
|
22
|
+
* Not `metrics.proven`, which is what the tile used to show. That counter is honestly
|
|
23
|
+
* named for what it holds — assessments resting on at least one piece of proven evidence —
|
|
24
|
+
* and evidence has a direction: a client holding an operator or interaction clearance
|
|
25
|
+
* produces *certain human* evidence, so it lands there too. A dashboard watching nothing
|
|
26
|
+
* but cleared humans therefore read "Proven bots: 100% of traffic", and the same requests
|
|
27
|
+
* were counted again under Unremarkable.
|
|
28
|
+
*
|
|
29
|
+
* The two proven-bot verdicts are the exact answer. Only the proven path can reach them —
|
|
30
|
+
* the probabilistic path produces `suspected-bot`, `human` or `unknown` and nothing else —
|
|
31
|
+
* so this is a rename of the truth rather than an approximation of it.
|
|
32
|
+
*/
|
|
33
|
+
export declare function provenBots(verdicts: Record<string, number>): number;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pager both tables use.
|
|
3
|
+
*
|
|
4
|
+
* One renderer rather than two, because the feed pages in the browser and the Actors
|
|
5
|
+
* table pages on the server and *neither of those facts belongs in the control*. What it
|
|
6
|
+
* needs to know is where you are and what it should do when you press something.
|
|
7
|
+
*
|
|
8
|
+
* Rendered twice per table, above and below. A table of fifty rows is taller than the
|
|
9
|
+
* window on most screens, and a pager only at the bottom means scrolling to the end to go
|
|
10
|
+
* back to the top of the next page.
|
|
11
|
+
*/
|
|
12
|
+
export interface PagerModel {
|
|
13
|
+
/** Zero-based. */
|
|
14
|
+
page: number;
|
|
15
|
+
/** What the range reads: `1–50 of 212`. Total omitted where the total is not known. */
|
|
16
|
+
from: number;
|
|
17
|
+
to: number;
|
|
18
|
+
total?: number;
|
|
19
|
+
atStart: boolean;
|
|
20
|
+
atEnd: boolean;
|
|
21
|
+
/** Shown next to the range when the list is being held still. */
|
|
22
|
+
held?: string;
|
|
23
|
+
go: (page: number) => void;
|
|
24
|
+
size?: {
|
|
25
|
+
current: number;
|
|
26
|
+
choices: readonly number[];
|
|
27
|
+
set: (size: number) => void;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare function renderPager(host: HTMLElement, model: PagerModel, options: {
|
|
31
|
+
withSize: boolean;
|
|
32
|
+
}): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export declare function drawTiles(force?: boolean): void;
|
|
2
|
+
/**
|
|
3
|
+
* The configuration facts at the end of the tab strip.
|
|
4
|
+
*
|
|
5
|
+
* Label first, value second, every time — "suspect at 60", not "60 suspect at" — and
|
|
6
|
+
* as plain text, which is the one treatment nothing else on the page uses for a
|
|
7
|
+
* control.
|
|
8
|
+
*/
|
|
9
|
+
export declare function drawChips(): void;
|
|
10
|
+
/**
|
|
11
|
+
* Every panel that counts the retained ring says how much ring there is.
|
|
12
|
+
*
|
|
13
|
+
* Half the Statistics screen counts the window and half counts since the process
|
|
14
|
+
* started; they were wearing the same grey subtitle, which invited exactly the
|
|
15
|
+
* comparison that does not hold.
|
|
16
|
+
*/
|
|
17
|
+
export declare function updateWindowLabels(): void;
|
|
18
|
+
export declare function drawLivePanels(): void;
|
|
19
|
+
export declare function drawStatsPanels(): void;
|
|
20
|
+
/**
|
|
21
|
+
* The audit panel: the window against its baseline.
|
|
22
|
+
*
|
|
23
|
+
* Both, side by side, because that is the only way the audit's output means anything.
|
|
24
|
+
* A bot share of 60% is a number; a bot share of 60% against a baseline of 12% is an
|
|
25
|
+
* incident.
|
|
26
|
+
*/
|
|
27
|
+
export declare function drawAudit(): void;
|
|
28
|
+
/** The count on the Policy tab. Cheap, so it runs on every frame; the panel itself does not. */
|
|
29
|
+
export declare function drawNoticeBadge(): void;
|
|
30
|
+
export declare function drawNotices(): void;
|
|
31
|
+
/**
|
|
32
|
+
* What somebody did to this process, and when.
|
|
33
|
+
*
|
|
34
|
+
* The same list the traffic timeline marks, written out — because a marker answers
|
|
35
|
+
* "was there a change here?" and a person auditing wants "what were they, in order".
|
|
36
|
+
* Bounded and in memory, which the panel says: the durable copy is the change events.
|
|
37
|
+
*/
|
|
38
|
+
export declare function drawChanges(): void;
|
|
39
|
+
/** Sibling instances, when the operator listed any. */
|
|
40
|
+
export declare function drawPeers(): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { DashboardEntry } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The rule editor works on a model — an array of plain rule objects — and both views
|
|
4
|
+
* render from it. The GUI mutates the model in place on each keystroke, which is what
|
|
5
|
+
* keeps focus and cursor position while somebody types; only structural changes (add,
|
|
6
|
+
* remove, move, a different action) re-render a card.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadPolicy(): Promise<void>;
|
|
9
|
+
export declare function drawPolicyTab(): void;
|
|
10
|
+
/**
|
|
11
|
+
* Starts a rule from a request the operator was looking at.
|
|
12
|
+
*
|
|
13
|
+
* Appended, expanded, and previewed straight away — the three things somebody would
|
|
14
|
+
* otherwise do by hand between noticing an actor and having a rule they can judge.
|
|
15
|
+
* Nothing is applied: this fills the editor, exactly like an import.
|
|
16
|
+
*
|
|
17
|
+
* It loads the policy document first, and that `await` is load-bearing rather than
|
|
18
|
+
* tidy. Drafting is reachable from the Live tab, which somebody can use for an hour
|
|
19
|
+
* without ever opening the Policy one — and appending to an editor that has not been
|
|
20
|
+
* filled yet leaves it holding the draft *and nothing else*, while marking it dirty so
|
|
21
|
+
* the load that follows will not correct it. Apply that and the running policy becomes
|
|
22
|
+
* one drafted rule.
|
|
23
|
+
*/
|
|
24
|
+
export declare function draftIntoEditor(entry: DashboardEntry): Promise<void>;
|
|
25
|
+
export declare function initPolicy(): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { DashboardEntry } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The feed's search.
|
|
4
|
+
*
|
|
5
|
+
* It used to be one `indexOf` over every field of a request joined into a string,
|
|
6
|
+
* which answers "does this word appear anywhere" and nothing else. That is the wrong
|
|
7
|
+
* question in the two cases somebody actually reaches for the box: an address that
|
|
8
|
+
* also appears inside a User-Agent, and a path that is a prefix of ten others.
|
|
9
|
+
*
|
|
10
|
+
* So terms can name a field, and can be negated:
|
|
11
|
+
*
|
|
12
|
+
* ```text
|
|
13
|
+
* actor:203.0.113.4 -path:/health that address, except its health checks
|
|
14
|
+
* rule:no-scrapers action:tag the rule that fired, and what it settled on
|
|
15
|
+
* score:>70 -certain probabilistic traffic close to the line
|
|
16
|
+
* "GET /api/v2/orders" a phrase, spaces and all
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Every term must match — this is an `AND`, because that is what narrowing means and
|
|
20
|
+
* a filter box that quietly `OR`s is a filter box that lies. Anything that does not
|
|
21
|
+
* parse as a field term is matched against the whole request, so a plain word still
|
|
22
|
+
* behaves exactly as it did.
|
|
23
|
+
*
|
|
24
|
+
* Kept free of the DOM so it can be unit-tested, which is the point: this is the piece
|
|
25
|
+
* of the page most likely to be wrong in a way nobody notices.
|
|
26
|
+
*/
|
|
27
|
+
export interface Term {
|
|
28
|
+
/** The field named before the colon, already resolved to a canonical name. */
|
|
29
|
+
field: string | undefined;
|
|
30
|
+
value: string;
|
|
31
|
+
negated: boolean;
|
|
32
|
+
/** For numeric fields: how `value` should be compared. */
|
|
33
|
+
compare?: "<" | ">" | "=" | undefined;
|
|
34
|
+
}
|
|
35
|
+
export declare function parseQuery(input: string): Term[];
|
|
36
|
+
/** Everything about a request, as one lower-case string. What a free term is matched against. */
|
|
37
|
+
export declare function searchableText(entry: DashboardEntry): string;
|
|
38
|
+
/** True when every term is satisfied. An empty query matches everything. */
|
|
39
|
+
export declare function matchesQuery(terms: readonly Term[], entry: DashboardEntry, haystack: string): boolean;
|
|
40
|
+
/** The named filter buttons, which are a second, independent narrowing. */
|
|
41
|
+
export type FilterName = "all" | "proven" | "suspected" | "human" | "guard" | "deny" | "mitigate" | "allow";
|
|
42
|
+
export declare function matchesFilter(filter: FilterName, entry: DashboardEntry): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DashboardEntry } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* A request as a line `bothandlerjs replay` can read.
|
|
4
|
+
*
|
|
5
|
+
* The point of the button this backs: a verdict you disagree with on screen becomes a
|
|
6
|
+
* fixture you can re-run offline, and then a corpus case that stops it coming back.
|
|
7
|
+
* Values the dashboard redacted stay redacted — the shape is what replays, and a
|
|
8
|
+
* session cookie is not part of the shape.
|
|
9
|
+
*/
|
|
10
|
+
export declare function replayLine(entry: DashboardEntry): string;
|
|
11
|
+
/** Every shown request as replay JSONL, oldest first — the whole filtered window at once. */
|
|
12
|
+
export declare function replayFile(entries: readonly DashboardEntry[]): string;
|
|
13
|
+
/** The same request as a traffic-corpus case, ready to paste into a fixture file. */
|
|
14
|
+
export declare function corpusCase(entry: DashboardEntry): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Preview } from "./types.js";
|
|
2
|
+
/** The result box under the editor. One box, whichever of the two forms wrote to it. */
|
|
3
|
+
export declare function showResult(kind: "ok" | "bad" | "warn", build: (box: HTMLElement) => void): void;
|
|
4
|
+
/**
|
|
5
|
+
* What a candidate would have done to the traffic still in the window.
|
|
6
|
+
*
|
|
7
|
+
* `newDenials` is called out on its own because it is the one direction of change that
|
|
8
|
+
* costs somebody their access, and it is easy to miss in a table of counts.
|
|
9
|
+
*/
|
|
10
|
+
export declare function renderPreview(preview: Preview, notes?: readonly string[]): void;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { EditorRule } from "./draft.js";
|
|
2
|
+
import type { FilterName, Term } from "./query.js";
|
|
3
|
+
import type { ActorRow, DashboardEntry, Policy, Row, Snapshot, TabName } from "./types.js";
|
|
4
|
+
/** Rows built into the table at once. Beyond this a feed is scrolled past, not read. */
|
|
5
|
+
export declare const FEED_LIMIT = 300;
|
|
6
|
+
export interface State {
|
|
7
|
+
rows: Row[];
|
|
8
|
+
byId: Map<string, Row>;
|
|
9
|
+
snapshot: Snapshot | undefined;
|
|
10
|
+
policy: Policy | undefined;
|
|
11
|
+
/** The Actors screen's list, fetched rather than streamed. See `registry.ts`. */
|
|
12
|
+
actors: ActorRow[];
|
|
13
|
+
actorsTracked: number;
|
|
14
|
+
paused: boolean;
|
|
15
|
+
filter: FilterName;
|
|
16
|
+
search: string;
|
|
17
|
+
terms: Term[];
|
|
18
|
+
tab: TabName;
|
|
19
|
+
open: Set<string>;
|
|
20
|
+
actor: string | undefined;
|
|
21
|
+
rangeMs: number;
|
|
22
|
+
/** Which population the score distribution is drawn from. See `charts.drawScores`. */
|
|
23
|
+
scoreScope: "run" | "window";
|
|
24
|
+
editorRules: EditorRule[];
|
|
25
|
+
editorDirty: boolean;
|
|
26
|
+
editorMode: "gui" | "json";
|
|
27
|
+
guardDirty: boolean;
|
|
28
|
+
/** Rows that arrived while paused, so the button can say what resuming will show. */
|
|
29
|
+
bufferedWhilePaused: number;
|
|
30
|
+
/** Frames the server dropped because *this* connection was too slow to take them. */
|
|
31
|
+
laggedDrops: number;
|
|
32
|
+
/**
|
|
33
|
+
* Which page of the feed is showing, newest first, zero-based.
|
|
34
|
+
*
|
|
35
|
+
* Zero follows the live feed. Any other page is a position in history, so arriving
|
|
36
|
+
* requests must not shuffle it under the reader — see `feedFrozen`.
|
|
37
|
+
*/
|
|
38
|
+
feedPage: number;
|
|
39
|
+
/**
|
|
40
|
+
* The matching rows as they stood when the reader left page zero.
|
|
41
|
+
*
|
|
42
|
+
* Without this, one request arriving while somebody reads page three moves every row
|
|
43
|
+
* down by one and they are silently reading different rows than the ones they were
|
|
44
|
+
* looking at. Frozen on leaving page zero, dropped on returning to it.
|
|
45
|
+
*/
|
|
46
|
+
feedFrozen: Row[] | undefined;
|
|
47
|
+
/** Which page of the Actors table is showing. Paged on the server, by offset. */
|
|
48
|
+
actorsPage: number;
|
|
49
|
+
/** How many rows a page of each table holds. Chosen in the page, not configured. */
|
|
50
|
+
feedPageSize: number;
|
|
51
|
+
actorsPageSize: number;
|
|
52
|
+
/**
|
|
53
|
+
* How many skipped entries have already been fetched back and merged.
|
|
54
|
+
*
|
|
55
|
+
* The server's `skipped` only ever grows, so the badge subtracts this to say how many
|
|
56
|
+
* are *still* missing rather than how many ever were.
|
|
57
|
+
*/
|
|
58
|
+
caughtUp: number;
|
|
59
|
+
}
|
|
60
|
+
export declare const state: State;
|
|
61
|
+
/**
|
|
62
|
+
* Files one entry into the ring.
|
|
63
|
+
*
|
|
64
|
+
* The same request arrives twice — once when it is assessed and again when the
|
|
65
|
+
* decision lands, and again after a reconnect that replays what was missed — so this
|
|
66
|
+
* is an upsert keyed by request id, and `rev` is what tells the renderer that a row on
|
|
67
|
+
* screen is now stale. Rebuilding a row nothing changed about is what used to wipe a
|
|
68
|
+
* text selection every time a request came in.
|
|
69
|
+
*/
|
|
70
|
+
export declare function ingest(entry: DashboardEntry): void;
|
|
71
|
+
export declare function clearFeed(): void;
|
|
72
|
+
export declare function setSearch(value: string): void;
|
|
73
|
+
/** Back to the live first page, thawed. Called whenever what matches changes. */
|
|
74
|
+
export declare function resetPaging(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Puts the ring back in time order after a bulk merge.
|
|
77
|
+
*
|
|
78
|
+
* `ingest` appends, because the stream delivers in order and appending is what that
|
|
79
|
+
* costs. A backlog fetched over HTTP is not in order relative to what is already held —
|
|
80
|
+
* it is *older* — so merging without this leaves yesterday's requests sitting at the
|
|
81
|
+
* newest end, which is where the feed reads from.
|
|
82
|
+
*/
|
|
83
|
+
export declare function sortRows(): void;
|
|
84
|
+
export declare function matches(row: Row): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Every row matching the filter, newest first.
|
|
87
|
+
*
|
|
88
|
+
* The whole set rather than a screenful: the feed pages through it, and the export means
|
|
89
|
+
* "what I am looking at" rather than "the first page of it".
|
|
90
|
+
*/
|
|
91
|
+
export declare function matchingRows(limit?: number): Row[];
|
|
92
|
+
/** The rows the feed would draw, newest first. Also what "export what I am looking at" means. */
|
|
93
|
+
export declare function visibleRows(limit?: number): Row[];
|
|
94
|
+
/**
|
|
95
|
+
* One page of the feed, and what the pager needs to describe itself.
|
|
96
|
+
*
|
|
97
|
+
* Page zero reads live and is recomputed every draw. Any other page reads the list as it
|
|
98
|
+
* was when the reader left page zero, because a feed that renumbers itself under somebody
|
|
99
|
+
* paging through it is a feed they cannot read.
|
|
100
|
+
*/
|
|
101
|
+
export declare function feedPage(size: number): {
|
|
102
|
+
rows: Row[];
|
|
103
|
+
page: number;
|
|
104
|
+
pages: number;
|
|
105
|
+
total: number;
|
|
106
|
+
};
|
|
107
|
+
/** Moves to a page, freezing the list on the way off page zero and thawing on the way back. */
|
|
108
|
+
export declare function goToFeedPage(page: number): void;
|
|
109
|
+
export declare function matchingCount(): number;
|
|
110
|
+
export declare function oldestAt(): number | undefined;
|
|
111
|
+
export interface Aggregates {
|
|
112
|
+
detectors: Map<string, number>;
|
|
113
|
+
actors: Map<string, number>;
|
|
114
|
+
identities: Map<string, number>;
|
|
115
|
+
paths: Map<string, number>;
|
|
116
|
+
deniedPaths: Map<string, number>;
|
|
117
|
+
guardStops: Map<string, number>;
|
|
118
|
+
ruleHits: Map<string, number>;
|
|
119
|
+
bypassed: Map<string, number>;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Everything the window-scoped panels count, in one pass over the ring.
|
|
123
|
+
*
|
|
124
|
+
* One pass rather than eight, and — more to the point — it is now called only by the
|
|
125
|
+
* screen that draws them. It used to run on every frame of the live feed to fill in
|
|
126
|
+
* panels on a tab nobody was looking at.
|
|
127
|
+
*/
|
|
128
|
+
export declare function aggregate(rows: readonly Row[]): Aggregates;
|