whisper-id 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/README.md +87 -76
- package/index.d.ts +84 -30
- package/index.js +134 -71
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,34 +1,103 @@
|
|
|
1
1
|
# whisper-id
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
**Two-tier, by design** (Postel's Law): **with no key** the keyless half works everywhere.
|
|
6
|
-
`verify`, `verifyDetails`, and `rdap` let anyone prove an address is a Whisper agent. **With
|
|
7
|
-
your key** (`opts.apiKey` or `WHISPER_API_KEY`) the whole control plane unlocks (list, policy,
|
|
8
|
-
logs, revoke, identity, agent), plus the keyed **`graph`** namespace (Cypher intelligence over
|
|
9
|
-
the Whisper Security graph) and **egress** that sources your traffic from your `/128`.
|
|
3
|
+
Three things for any Node agent, in one dependency-free package: query the **Whisper security graph** (3.6B nodes / 30B relationships, Cypher), give the agent a routable **IPv6 identity** with safe **egress**, and drive the full **control plane**.
|
|
10
4
|
|
|
11
5
|
```sh
|
|
12
6
|
npm i whisper-id
|
|
13
7
|
```
|
|
14
8
|
|
|
9
|
+
## The security graph (keyless, zero setup)
|
|
10
|
+
|
|
11
|
+
The Whisper graph knows who operates a host, its threat posture, its look-alikes, the real origins behind a CDN, WHOIS history, and 20+ named investigations. The **direct read verbs run with no key at all** (rate-limited taste, ~100/window). One import, real answers:
|
|
12
|
+
|
|
15
13
|
```js
|
|
16
|
-
import {
|
|
17
|
-
|
|
14
|
+
import { graph } from "whisper-id"; // no key needed for the read verbs
|
|
15
|
+
|
|
16
|
+
await graph.assess("8.8.8.8"); // -> { rows: [{ host: "8.8.8.8", label: "benign-allowlisted", band: "INFO", ... }] }
|
|
17
|
+
await graph.identify("api.openai.com"); // who operates this host -> vendor + operator roles
|
|
18
|
+
await graph.origins("cloudflare.com"); // the real origin IPs behind a CDN
|
|
19
|
+
await graph.explain("paypal.com"); // threat-feed score + why
|
|
18
20
|
```
|
|
19
21
|
|
|
22
|
+
Set `WHISPER_API_KEY` (or pass `{ apiKey }`) to lift the rate limit and unlock **raw Cypher** and the **multi-step flows**:
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
// raw Cypher, your own query, parameters bound as $-params (never string-built):
|
|
26
|
+
await graph.query("MATCH (h:HOSTNAME {name:$n})-[:RESOLVES_TO]->(ip) RETURN ip.name AS ip LIMIT 5", { n: "github.com" });
|
|
27
|
+
|
|
28
|
+
// a named catalog recipe (a multi-step investigation, streamed over SSE):
|
|
29
|
+
await graph.typosquat("paypal.com"); // look-alike sweep -> registered variants + verdict
|
|
30
|
+
await graph.attackSurface({ domain: "github.com" }); // full external footprint, scored
|
|
31
|
+
|
|
32
|
+
// discover the whole catalog (29 queries + flows) with no key, no network:
|
|
33
|
+
for (const r of graph.recipes()) {
|
|
34
|
+
console.log(r.method, r.keyless ? "keyless" : "keyed", r.docsUrl);
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Every verb maps to a catalog entry with its own docs page under [whisper.security/docs](https://www.whisper.security/docs) (e.g. [`assess`](https://www.whisper.security/docs/whisper-graph/procedures), [`identify`](https://www.whisper.security/docs/whisper-graph/procedures/identify)); `recipes()` carries the exact `docsUrl` for each, and the same URL sits on every method as `graph.identify.docs` and on its `@see` tag in your editor. The 13 direct reads are keyless; the 15 multi-step flows and the `submit` write channel are keyed. Full query reference: [whisper-catalog](https://github.com/whisper-sec/whisper-catalog).
|
|
39
|
+
|
|
40
|
+
## Identity + egress
|
|
41
|
+
|
|
20
42
|
```js
|
|
21
43
|
import { identity, withEgress } from "whisper-id"; // with a key (WHISPER_API_KEY)
|
|
44
|
+
|
|
22
45
|
const me = await identity({ label: "my-bot" }); // allocate a routable Whisper /128
|
|
23
46
|
await withEgress(async (e) => {
|
|
24
47
|
// proxy env (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY) now points at your local Whisper proxy
|
|
25
|
-
//
|
|
48
|
+
// -> curl, axios, got, and other env-aware clients leave from your /128.
|
|
26
49
|
console.log("egress on", e.proxyUrl);
|
|
27
50
|
});
|
|
28
51
|
```
|
|
29
52
|
|
|
53
|
+
And anyone, with no key, can verify an address is a real Whisper agent:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import { verify, rdap } from "whisper-id";
|
|
57
|
+
if (await verify(addr)) console.log((await rdap(addr)).name); // keyless, pure HTTPS
|
|
58
|
+
```
|
|
59
|
+
|
|
30
60
|
## API
|
|
31
61
|
|
|
62
|
+
### The graph (two-tier)
|
|
63
|
+
|
|
64
|
+
`import { graph } from "whisper-id"`: one typed method per catalog entry, plus `graph.query`
|
|
65
|
+
(raw Cypher) and `graph.recipes()` (catalog discovery). Direct reads resolve to
|
|
66
|
+
`{ columns, rows, statistics }` where `rows` are objects keyed by column name.
|
|
67
|
+
|
|
68
|
+
| Call | Keyless | Does |
|
|
69
|
+
|------|:-------:|------|
|
|
70
|
+
| `graph.assess(value)` | yes | Labelled threat posture for a host or IP (malicious / benign / unknown) |
|
|
71
|
+
| `graph.identify(value)` | yes | Name the vendor and operator role behind a host or IP |
|
|
72
|
+
| `graph.variants(value)` | yes | Look-alike domain variants of a brand, and which are registered |
|
|
73
|
+
| `graph.walk(value)` | yes | Walk to the nearest known vendors behind a host, with channel + confidence |
|
|
74
|
+
| `graph.explain(value)` | yes | Score an indicator against the threat feeds and explain why |
|
|
75
|
+
| `graph.origins(value)` | yes | Real origin IPs behind a CDN-fronted domain, ranked by confidence |
|
|
76
|
+
| `graph.history(value)` / `graph.historyWhois(value)` | yes | Historical WHOIS timeline for a domain |
|
|
77
|
+
| `graph.pslTldplusone(value)` / `graph.pslAffiliation(value)` | yes | Registrable apex (eTLD+1) / PSL private-section affiliation |
|
|
78
|
+
| `graph.asset(value)` | yes | Member ASNs of an AS-SET macro |
|
|
79
|
+
| `graph.lookupTorRelay(value)` | yes | Is an IP a known Tor exit relay |
|
|
80
|
+
| `graph.dbSchema()` | yes | Every node + relationship type in the graph, with counts and examples |
|
|
81
|
+
| `graph.query(cypher, params)` | key | Raw Cypher escape hatch. -> the full `{ columns, rows, statistics }` |
|
|
82
|
+
| `graph.submit({ kind, ...fields })` | key | Contribute an indicator observation or feedback (the write channel) |
|
|
83
|
+
| `graph.recipes()` | yes (offline) | The whole catalog: `{ method, keyless, mode, summary, params, docsUrl }` per verb |
|
|
84
|
+
|
|
85
|
+
Fifteen multi-step **workflow** methods (`attackPath`, `attackSurface`, `blastRadius`, `typosquat`,
|
|
86
|
+
`indicator`, `routeHealth`, and 9 more) execute keyed on the Whisper workflow runner and stream
|
|
87
|
+
Server-Sent Events; one call collects the whole run into `{ slug, steps, complete, events }`:
|
|
88
|
+
|
|
89
|
+
```js
|
|
90
|
+
const run = await graph.typosquat("paypal.com"); // bare value -> the flow's primary input
|
|
91
|
+
console.log(run.steps.length, "steps"); // each step: its bound Cypher, columns, rows
|
|
92
|
+
console.log(run.complete.totalLatencyMs, "ms"); // the terminal aggregate context
|
|
93
|
+
|
|
94
|
+
await graph.attackSurface({ domain: "github.com", level: "deep" }); // named inputs + knobs
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Every `graph` method is generated from the Whisper query catalog (never hand-copied, so it
|
|
98
|
+
cannot drift). A key, when present, rides only in the `X-API-Key` header; a keyless call sends
|
|
99
|
+
no auth header at all.
|
|
100
|
+
|
|
32
101
|
### Keyless: no key, no CLI (pure HTTPS)
|
|
33
102
|
|
|
34
103
|
Run anywhere `fetch` runs (Cloudflare Workers, Vercel, Deno, Lambda, the browser).
|
|
@@ -40,11 +109,6 @@ Run anywhere `fetch` runs (Cloudflare Workers, Vercel, Deno, Lambda, the browser
|
|
|
40
109
|
| `rdap(address)` | The public RDAP record for a `/128`, or `null` |
|
|
41
110
|
| `egressIp()` | The IP this process leaves from (a `/128` when routed, else the platform's) → `string` |
|
|
42
111
|
|
|
43
|
-
```js
|
|
44
|
-
import { verify, rdap } from "whisper-id";
|
|
45
|
-
if (await verify(addr)) console.log((await rdap(addr)).name);
|
|
46
|
-
```
|
|
47
|
-
|
|
48
112
|
### Control plane: with a key (pure HTTP, no CLI)
|
|
49
113
|
|
|
50
114
|
Pure `fetch` to the control endpoint, with no `whisper` CLI and no child process. The key comes from
|
|
@@ -69,62 +133,6 @@ for (const a of await list()) console.log(a.address, a.label);
|
|
|
69
133
|
const recent = await logs({ kind: "dns", limit: 100 });
|
|
70
134
|
```
|
|
71
135
|
|
|
72
|
-
### Graph (keyed Cypher, pure HTTP)
|
|
73
|
-
|
|
74
|
-
The intelligence half of the keyed surface. `import { graph } from "whisper-id"` and you get one
|
|
75
|
-
typed method per catalog entry over the Whisper Security graph, plus a raw `graph.query` escape
|
|
76
|
-
hatch. Same auth as the control plane: the key comes from `opts.apiKey` or `WHISPER_API_KEY` and
|
|
77
|
-
is sent only in `X-API-Key`. If it is Cypher, it needs a key: this whole namespace is keyed (the
|
|
78
|
-
keyless `verify` / `verifyDetails` / `rdap` above touch no Cypher and stay key-free). Every method
|
|
79
|
-
resolves to `{ columns, rows, statistics }` where `rows` are objects keyed by column name.
|
|
80
|
-
|
|
81
|
-
```js
|
|
82
|
-
import { graph } from "whisper-id"; // WHISPER_API_KEY in the env, or pass { apiKey }
|
|
83
|
-
|
|
84
|
-
const r = await graph.identify("api.openai.com", { apiKey: "whisper-..." });
|
|
85
|
-
console.log(r.rows[0].vendor_id, r.rows[0].canonical_name); // → openai OpenAI
|
|
86
|
-
|
|
87
|
-
await graph.assess("185.220.101.33"); // threat posture: malicious / benign / unknown
|
|
88
|
-
await graph.variants("paypal.com"); // registered look-alike domains
|
|
89
|
-
await graph.origins("cloudflare.com"); // real origin IPs behind a CDN
|
|
90
|
-
await graph.query("MATCH (n) RETURN n LIMIT $lim", { lim: 5 }); // raw Cypher escape hatch
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
| Call | Does |
|
|
94
|
-
|------|------|
|
|
95
|
-
| `graph.identify(value)` | Name the vendor and operator role behind a host or IP |
|
|
96
|
-
| `graph.assess(value)` | Labelled threat posture for a host or IP (malicious / benign / unknown) |
|
|
97
|
-
| `graph.variants(value)` | Look-alike domain variants of a brand, and which are registered |
|
|
98
|
-
| `graph.walk(value)` | Walk to the nearest known vendors behind a host, with channel + confidence |
|
|
99
|
-
| `graph.explain(value)` | Score an indicator against the threat feeds and explain why |
|
|
100
|
-
| `graph.pslTldplusone(value)` | Reduce any hostname to its registrable apex (eTLD+1) |
|
|
101
|
-
| `graph.pslAffiliation(value)` | Is a domain a PSL private-section suffix, and who submitted it |
|
|
102
|
-
| `graph.origins(value)` | Real origin IPs behind a CDN-fronted domain, ranked by confidence |
|
|
103
|
-
| `graph.history(value)` | Full historical WHOIS timeline for a domain |
|
|
104
|
-
| `graph.historyWhois(value)` | WHOIS-only historical timeline for a domain |
|
|
105
|
-
| `graph.asset(value)` | Member ASNs of an AS-SET macro |
|
|
106
|
-
| `graph.lookupTorRelay(value)` | Is an IP a known Tor exit relay |
|
|
107
|
-
| `graph.dbSchema()` | Every node + relationship type in the graph, with counts and examples |
|
|
108
|
-
| `graph.submit({ kind, ...fields })` | Contribute an indicator observation or feedback (keyed write) |
|
|
109
|
-
| `graph.query(cypher, params)` | Raw Cypher escape hatch. → the full `{ columns, rows, statistics }` |
|
|
110
|
-
|
|
111
|
-
Fifteen multi-step **workflow** methods (`attackPath`, `attackSurface`, `blastRadius`, `typosquat`,
|
|
112
|
-
`indicator`, `routeHealth`, and 9 more) execute on the Whisper workflow runner and stream
|
|
113
|
-
Server-Sent Events; one call collects the whole run into `{ slug, steps, complete, events }`:
|
|
114
|
-
|
|
115
|
-
```js
|
|
116
|
-
const run = await graph.typosquat("paypal.com"); // bare value → the flow's primary input
|
|
117
|
-
console.log(run.steps.length, "steps"); // each step: its bound Cypher, columns, rows
|
|
118
|
-
console.log(run.complete.totalLatencyMs, "ms"); // the terminal aggregate context
|
|
119
|
-
|
|
120
|
-
await graph.attackSurface({ domain: "github.com", level: "deep" }); // named inputs + knobs
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
Every `graph` method is generated from the Whisper query catalog (never hand-copied, so it
|
|
124
|
-
cannot drift) and links its canonical docs: `graph.identify.docs` →
|
|
125
|
-
`https://www.whisper.security/docs/whisper-graph/procedures/identify`, and the same URL is on
|
|
126
|
-
each method's `@see` tag in your editor.
|
|
127
|
-
|
|
128
136
|
### Egress + CLI-backed
|
|
129
137
|
|
|
130
138
|
| Call | Does |
|
|
@@ -135,7 +143,7 @@ each method's `@see` tag in your editor.
|
|
|
135
143
|
| `ip()` | Your current egress IP via the CLI, proving it's your `/128`. → `string` |
|
|
136
144
|
|
|
137
145
|
`register`, `egress`, `withEgress`, and `ip` need the `whisper` CLI on the host (egress brings up
|
|
138
|
-
the local proxy). Everything
|
|
146
|
+
the local proxy). Everything else, the graph included, is pure HTTP and needs no CLI.
|
|
139
147
|
|
|
140
148
|
### Using it with `fetch`
|
|
141
149
|
|
|
@@ -155,9 +163,10 @@ transparently inside `withEgress`.
|
|
|
155
163
|
|
|
156
164
|
## Requirements
|
|
157
165
|
|
|
158
|
-
Zero dependencies. The **keyless** checks and the **control plane** are
|
|
159
|
-
global `fetch`), with no CLI and nothing to install.
|
|
160
|
-
pass `{ apiKey }`)
|
|
166
|
+
Zero dependencies. The **graph read verbs**, the **keyless** checks, and the **control plane** are
|
|
167
|
+
pure HTTP (Node ≥ 18's global `fetch`), with no CLI and nothing to install. Set `WHISPER_API_KEY`
|
|
168
|
+
(or pass `{ apiKey }`) for the control plane and the keyed graph (raw Cypher + flows + `submit`);
|
|
169
|
+
the graph read verbs, `verify`, and `rdap` need no key.
|
|
161
170
|
|
|
162
171
|
Only **egress** and the CLI-backed calls (`register`, `egress`, `withEgress`, `ip`) need the
|
|
163
172
|
`whisper` CLI on your `PATH`:
|
|
@@ -167,11 +176,13 @@ curl get.whisper.online | sh
|
|
|
167
176
|
```
|
|
168
177
|
|
|
169
178
|
(`$WHISPER_BIN` overrides the CLI path; `$WHISPER_CONTROL_URL` / `$WHISPER_GRAPH_URL` /
|
|
170
|
-
`$WHISPER_RDAP_URL` override the endpoints.)
|
|
179
|
+
`$WHISPER_FLOW_URL` / `$WHISPER_RDAP_URL` override the endpoints.)
|
|
171
180
|
|
|
172
181
|
## Links
|
|
173
182
|
|
|
174
183
|
- Site: https://whisper.online
|
|
184
|
+
- Docs: https://www.whisper.security/docs
|
|
185
|
+
- Query catalog: https://github.com/whisper-sec/whisper-catalog
|
|
175
186
|
- CLI: https://github.com/whisper-sec/whisper-cli
|
|
176
187
|
|
|
177
188
|
MIT licensed.
|
package/index.d.ts
CHANGED
|
@@ -151,19 +151,36 @@ export function identity(opts?: IdentityOptions): Promise<AgentItem | null>;
|
|
|
151
151
|
export function agent(sel: string, opts?: ControlOptions): Promise<AgentItem | null>;
|
|
152
152
|
export function agent(opts?: AgentSelectOptions): Promise<AgentItem | null>;
|
|
153
153
|
|
|
154
|
-
// ── Graph (
|
|
155
|
-
// The intelligence
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
// workflow runner
|
|
160
|
-
//
|
|
161
|
-
//
|
|
162
|
-
// `@see` URL and are also on the
|
|
163
|
-
|
|
164
|
-
|
|
154
|
+
// ── Graph (the Whisper Security graph: Cypher intelligence, two-tier) ────────────────
|
|
155
|
+
// The intelligence pillar, Postel two-tier with both halves honest: the 13 direct READ
|
|
156
|
+
// verbs (identify, assess, variants, walk, explain, origins, history, and friends) run
|
|
157
|
+
// KEYLESS - no key at all, a rate-limited taste with real answers; a key ({ apiKey } or
|
|
158
|
+
// WHISPER_API_KEY) lifts the limit. Raw Cypher (`graph.query`), the 15 multi-step `flow`
|
|
159
|
+
// methods (workflow runner: POST gallery/run, Server-Sent Events), and the `submit`
|
|
160
|
+
// write stay KEYED. A present key rides only in `X-API-Key`; an absent one sends no auth
|
|
161
|
+
// header at all. The method surface below is GENERATED from the Whisper query catalog by
|
|
162
|
+
// scripts/gen-graph.mjs; every method's docs live at the `@see` URL and are also on the
|
|
163
|
+
// function itself as `.docs`, and `graph.recipes()` lists the whole catalog.
|
|
164
|
+
|
|
165
|
+
/** Per-call options for the graph namespace (same shape as the control plane). */
|
|
165
166
|
export interface GraphOptions extends ControlOptions {}
|
|
166
167
|
|
|
168
|
+
/** One catalog entry from `graph.recipes()`: a graph method and its two-tier access. */
|
|
169
|
+
export interface GraphRecipe {
|
|
170
|
+
/** The method name on the `graph` namespace (e.g. "assess", "typosquat"). */
|
|
171
|
+
method: string;
|
|
172
|
+
/** True for the 13 direct read verbs that run with no key at all (rate-limited). */
|
|
173
|
+
keyless: boolean;
|
|
174
|
+
/** "direct" (one Cypher read/write) or "flow" (multi-step, workflow runner). */
|
|
175
|
+
mode: "direct" | "flow";
|
|
176
|
+
/** What the method answers, in one line. */
|
|
177
|
+
summary: string;
|
|
178
|
+
/** The method's input names (a flow's inputs, a direct verb's $-parameter, the submit fields). */
|
|
179
|
+
params: string[];
|
|
180
|
+
/** The canonical docs page for this method. */
|
|
181
|
+
docsUrl: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
167
184
|
/** Result of a keyed graph query. `rows` are objects keyed by column name. */
|
|
168
185
|
export interface GraphResult {
|
|
169
186
|
columns: string[];
|
|
@@ -220,18 +237,29 @@ export interface GraphFlowResult {
|
|
|
220
237
|
}
|
|
221
238
|
|
|
222
239
|
/**
|
|
223
|
-
* The
|
|
224
|
-
* 15 workflow-runner flows), plus a raw `query` escape hatch
|
|
225
|
-
*
|
|
240
|
+
* The graph namespace: one typed method per catalog entry (14 direct Cypher reads +
|
|
241
|
+
* 15 workflow-runner flows), plus a raw `query` escape hatch and `recipes()` discovery.
|
|
242
|
+
* Two-tier: the 13 direct read verbs run keyless (no key needed, rate-limited; a key
|
|
243
|
+
* lifts the limit); `query`, the flows, and `submit` need a key (`{ apiKey }` or
|
|
244
|
+
* `WHISPER_API_KEY`). Every method carries its canonical docs URL as `.docs`.
|
|
226
245
|
*/
|
|
227
246
|
export interface GraphNamespace {
|
|
228
247
|
/**
|
|
229
|
-
* Raw Cypher escape hatch: run arbitrary Cypher with `params` bound
|
|
230
|
-
* $-parameters. Returns the full `{columns,rows,statistics}`.
|
|
248
|
+
* Raw Cypher escape hatch (KEYED): run arbitrary Cypher with `params` bound
|
|
249
|
+
* server-side as $-parameters. Returns the full `{columns,rows,statistics}`.
|
|
231
250
|
* @see https://www.whisper.security/docs/cypher-api
|
|
232
251
|
*/
|
|
233
252
|
query(cypher: string, params?: Record<string, unknown>, opts?: GraphOptions): Promise<GraphResult>;
|
|
234
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Discover the whole catalog: every graph method with `{ method, keyless, mode,
|
|
256
|
+
* summary, params, docsUrl }`. Baked from the Whisper query catalog at generation
|
|
257
|
+
* time - no key, no network. `keyless: true` marks the direct read verbs that run
|
|
258
|
+
* with no key at all.
|
|
259
|
+
* @see https://www.whisper.security/docs/recipes
|
|
260
|
+
*/
|
|
261
|
+
recipes(): GraphRecipe[];
|
|
262
|
+
|
|
235
263
|
// BEGIN GENERATED: graph catalog (scripts/gen-graph.mjs) - do not edit by hand
|
|
236
264
|
/**
|
|
237
265
|
* Assess how resilient a country's core DNS is if it were cut off from the world.
|
|
@@ -437,7 +465,9 @@ export interface GraphNamespace {
|
|
|
437
465
|
* Resolves a host/IP to its canonical vendor, category and operator roles (DNS_OPERATOR / CDN
|
|
438
466
|
* / ORIGIN_AS / MAIL_RECEIVER) with a confidence band.
|
|
439
467
|
*
|
|
440
|
-
*
|
|
468
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
469
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
470
|
+
* Cypher: CALL whisper.identify([$v]) YIELD host, vendor_id, canonical_name, category, roles, host_class, band
|
|
441
471
|
* Returns columns: host, vendor_id, canonical_name, category, roles, host_class, band.
|
|
442
472
|
* @see https://www.whisper.security/docs/whisper-graph/procedures/identify
|
|
443
473
|
*/
|
|
@@ -449,7 +479,9 @@ export interface GraphNamespace {
|
|
|
449
479
|
* Returns a posture label + severity band + sub-labels + coverage + evidence for an indicator;
|
|
450
480
|
* benign-allowlisted vs malicious-evidenced, never a bare score.
|
|
451
481
|
*
|
|
452
|
-
*
|
|
482
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
483
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
484
|
+
* Cypher: CALL whisper.assess([$v]) YIELD host, label, band, sub_labels, coverage, evidence
|
|
453
485
|
* Returns columns: host, label, band, sub_labels, coverage, evidence.
|
|
454
486
|
* @see https://www.whisper.security/docs/whisper-graph/procedures
|
|
455
487
|
*/
|
|
@@ -461,7 +493,9 @@ export interface GraphNamespace {
|
|
|
461
493
|
* Enumerates permutations (omission, bitsquatting, ...) of a domain, flags which exist, with a
|
|
462
494
|
* per-variant confidence.
|
|
463
495
|
*
|
|
464
|
-
*
|
|
496
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
497
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
498
|
+
* Cypher: CALL whisper.variants($v) YIELD variant, method, exists, confidence
|
|
465
499
|
* Returns columns: variant, method, exists, confidence.
|
|
466
500
|
* @see https://www.whisper.security/docs/whisper-graph/procedures/variants
|
|
467
501
|
*/
|
|
@@ -474,7 +508,9 @@ export interface GraphNamespace {
|
|
|
474
508
|
* ORIGIN_AS) and confidence, plus siblings and coverage - explains WHY a host maps to a
|
|
475
509
|
* vendor.
|
|
476
510
|
*
|
|
477
|
-
*
|
|
511
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
512
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
513
|
+
* Cypher: CALL whisper.walk($v) YIELD coverage, host, nearest_known_vendors, no_atlas_match, siblings
|
|
478
514
|
* Returns columns: coverage, host, nearest_known_vendors, no_atlas_match, siblings.
|
|
479
515
|
* @see https://www.whisper.security/docs/whisper-graph/procedures
|
|
480
516
|
*/
|
|
@@ -487,7 +523,9 @@ export interface GraphNamespace {
|
|
|
487
523
|
* weight, firstSeen/lastSeen). Two live forms: bare CALL explain($v) (full columns) and CALL
|
|
488
524
|
* whisper.explain($v) (restricted YIELD).
|
|
489
525
|
*
|
|
490
|
-
*
|
|
526
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
527
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
528
|
+
* Cypher: CALL whisper.explain($v) YIELD indicator, score, level, explanation, sources
|
|
491
529
|
* Returns columns: indicator, score, level, explanation, sources.
|
|
492
530
|
* @see https://www.whisper.security/docs/whisper-graph/procedures/explain
|
|
493
531
|
*/
|
|
@@ -499,7 +537,9 @@ export interface GraphNamespace {
|
|
|
499
537
|
* PSL-correct eTLD+1: www.foo.co.uk -> foo.co.uk. The right way to group hosts by the thing
|
|
500
538
|
* someone actually registered.
|
|
501
539
|
*
|
|
502
|
-
*
|
|
540
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
541
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
542
|
+
* Cypher: CALL whisper.psl.tldPlusOne($v) YIELD apex
|
|
503
543
|
* Returns columns: apex.
|
|
504
544
|
* @see https://www.whisper.security/docs/whisper-graph/procedures/helpers
|
|
505
545
|
*/
|
|
@@ -511,7 +551,9 @@ export interface GraphNamespace {
|
|
|
511
551
|
* For a PSL private-section suffix, returns the submitting org/login and evidence kind +
|
|
512
552
|
* confidence; found=false for ordinary registrable domains.
|
|
513
553
|
*
|
|
514
|
-
*
|
|
554
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
555
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
556
|
+
* Cypher: CALL whisper.psl.affiliation($v) YIELD found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence
|
|
515
557
|
* Returns columns: found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence.
|
|
516
558
|
* @see https://www.whisper.security/docs/whisper-graph/procedures/helpers
|
|
517
559
|
*/
|
|
@@ -523,7 +565,9 @@ export interface GraphNamespace {
|
|
|
523
565
|
* Candidate origin IPs behind a CDN with confidence, the methods that found them (e.g.
|
|
524
566
|
* links_to), and the hosting ASN/name - de-cloaks to the real server.
|
|
525
567
|
*
|
|
526
|
-
*
|
|
568
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
569
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
570
|
+
* Cypher: CALL whisper.origins($v) YIELD ip, confidence, methods, asn, asnName, kind
|
|
527
571
|
* Returns columns: ip, confidence, methods, asn, asnName, kind.
|
|
528
572
|
* @see https://www.whisper.security/docs/whisper-graph/procedures/origins
|
|
529
573
|
*/
|
|
@@ -535,7 +579,9 @@ export interface GraphNamespace {
|
|
|
535
579
|
* Every observed WHOIS snapshot over time: create/update/expiry, registrar, registrant,
|
|
536
580
|
* country, nameservers - the ownership/registration story.
|
|
537
581
|
*
|
|
538
|
-
*
|
|
582
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
583
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
584
|
+
* Cypher: CALL whisper.history($v)
|
|
539
585
|
* Returns columns: indicator, type, queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers, cached.
|
|
540
586
|
* @see https://www.whisper.security/docs/whisper-graph/procedures/history
|
|
541
587
|
*/
|
|
@@ -547,7 +593,9 @@ export interface GraphNamespace {
|
|
|
547
593
|
* The registration-record projection of the history timeline: create/update/expiry, registrar,
|
|
548
594
|
* registrant, country, nameservers per snapshot.
|
|
549
595
|
*
|
|
550
|
-
*
|
|
596
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
597
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
598
|
+
* Cypher: CALL whisper.history.whois($v) YIELD queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers
|
|
551
599
|
* Returns columns: queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers.
|
|
552
600
|
* @see https://www.whisper.security/docs/whisper-graph/procedures/history
|
|
553
601
|
*/
|
|
@@ -560,7 +608,9 @@ export interface GraphNamespace {
|
|
|
560
608
|
* STRING as-set name, not an integer ASN. (Membership is sparsely populated in the current
|
|
561
609
|
* graph.)
|
|
562
610
|
*
|
|
563
|
-
*
|
|
611
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
612
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
613
|
+
* Cypher: CALL whisper.asSet($v) YIELD asSetName, memberAsn, sourceRir
|
|
564
614
|
* Returns columns: asSetName, memberAsn, sourceRir.
|
|
565
615
|
* @see https://www.whisper.security/docs/whisper-graph/procedures
|
|
566
616
|
*/
|
|
@@ -572,7 +622,9 @@ export interface GraphNamespace {
|
|
|
572
622
|
* Live Tor exit-node check: found + fingerprint + exit-address count + source + ingest time
|
|
573
623
|
* for an IPv4.
|
|
574
624
|
*
|
|
575
|
-
*
|
|
625
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
626
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
627
|
+
* Cypher: CALL whisper.lookupTorRelay($v) YIELD indicator, found, fingerprint, exitAddressCount, source, ingestedAt
|
|
576
628
|
* Returns columns: indicator, found, fingerprint, exitAddressCount, source, ingestedAt.
|
|
577
629
|
* @see https://www.whisper.security/docs/whisper-graph/procedures/helpers
|
|
578
630
|
*/
|
|
@@ -584,7 +636,9 @@ export interface GraphNamespace {
|
|
|
584
636
|
* The self-describing schema: node/relationship types with counts, descriptions, examples,
|
|
585
637
|
* source/target labels and query best-practices - the map of what the graph holds.
|
|
586
638
|
*
|
|
587
|
-
*
|
|
639
|
+
* KEYLESS direct read: runs with NO key at all (a rate-limited taste, real
|
|
640
|
+
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
641
|
+
* Cypher: CALL db.schema()
|
|
588
642
|
* Returns columns: type, name, count, description, example, sourceLabels, targetLabels, fastPatterns, slowPatterns, bestPractices.
|
|
589
643
|
* @see https://www.whisper.security/docs/whisper-graph/schema
|
|
590
644
|
*/
|
|
@@ -605,5 +659,5 @@ export interface GraphNamespace {
|
|
|
605
659
|
// END GENERATED: graph catalog
|
|
606
660
|
}
|
|
607
661
|
|
|
608
|
-
/** The
|
|
662
|
+
/** The Whisper security graph namespace (two-tier). `import { graph } from "whisper-id"`. */
|
|
609
663
|
export const graph: GraphNamespace;
|
package/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
2
|
// Copyright (c) 2026 viaGraph B.V. (Whisper Security)
|
|
3
|
-
// whisper-id - a routable IPv6 identity and safe egress for
|
|
4
|
-
//
|
|
5
|
-
//
|
|
3
|
+
// whisper-id - the Whisper security graph, a routable IPv6 identity, and safe egress for
|
|
4
|
+
// any Node agent. Dependency-free and two-tier (Postel): the graph's direct read verbs and
|
|
5
|
+
// the identity checks run KEYLESS over plain fetch() (graph.assess("8.8.8.8") needs no key
|
|
6
|
+
// at all); the control plane, raw Cypher, and the flows unlock with WHISPER_API_KEY; only
|
|
7
|
+
// register/egress/ip lean on the `whisper` CLI (https://whisper.online).
|
|
6
8
|
import { execFile } from "node:child_process";
|
|
7
9
|
import { promisify } from "node:util";
|
|
8
10
|
|
|
@@ -313,7 +315,7 @@ async function callControl(op, args, opts = {}) {
|
|
|
313
315
|
if (!key) {
|
|
314
316
|
throw new WhisperError(
|
|
315
317
|
`\`${op}\` needs a Whisper API key - pass { apiKey } or set WHISPER_API_KEY. ` +
|
|
316
|
-
"(verify/verifyDetails/rdap are keyless and need no key.)"
|
|
318
|
+
"(verify/verifyDetails/rdap and the graph's direct read verbs are keyless and need no key.)"
|
|
317
319
|
);
|
|
318
320
|
}
|
|
319
321
|
const url = controlUrl();
|
|
@@ -455,23 +457,27 @@ export async function agent(sel = {}, maybeOpts) {
|
|
|
455
457
|
return recs.length ? recs[0] : null;
|
|
456
458
|
}
|
|
457
459
|
|
|
458
|
-
// ── Graph (
|
|
459
|
-
// The intelligence
|
|
460
|
-
//
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
// (
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
//
|
|
460
|
+
// ── Graph (the Whisper Security graph: Cypher intelligence, two-tier) ────────────────
|
|
461
|
+
// The intelligence pillar, Postel two-tier with both halves honest: the 13 DIRECT READ
|
|
462
|
+
// verbs (identify, assess, variants, walk, explain, pslTldplusone, pslAffiliation,
|
|
463
|
+
// origins, history, historyWhois, asset, lookupTorRelay, dbSchema) run KEYLESS - no key
|
|
464
|
+
// at all, a rate-limited taste (~100/window) with real answers; a key (opts.apiKey or
|
|
465
|
+
// WHISPER_API_KEY) lifts the limit. Raw Cypher (graph.query), the 15 multi-step flows,
|
|
466
|
+
// and the submit write channel stay KEYED. Everything POSTs to the SAME endpoint the
|
|
467
|
+
// control plane uses (https://graph.whisper.security/api/query), reusing resolveKey() so
|
|
468
|
+
// there is one auth path, not two: a present key rides ONLY in the X-API-Key header, an
|
|
469
|
+
// absent one sends NO auth header at all. It does NOT go through the whisper.agents()
|
|
470
|
+
// control wrapper: the graph contract is different (body {query,parameters}; reply
|
|
471
|
+
// {columns,rows,statistics} where rows are already column-keyed objects), so it has its
|
|
472
|
+
// own choke point, callGraph().
|
|
467
473
|
//
|
|
468
474
|
// Source of truth for the 29 methods is the Whisper query catalog (catalog.json +
|
|
469
475
|
// sdk-methods.json): the GRAPH_METHODS table below is GENERATED from it by
|
|
470
476
|
// scripts/gen-graph.mjs (`npm run gen`), so this SDK cannot drift from the catalog. The
|
|
471
477
|
// 14 `direct` entries run their catalog Cypher against /api/query; the 15 `flow` entries
|
|
472
478
|
// EXECUTE on the workflow runner (POST gallery/run, Server-Sent Events) via callFlow().
|
|
473
|
-
// Every method carries its canonical docs URL as `.docs
|
|
474
|
-
// escape hatch.
|
|
479
|
+
// Every method carries its canonical docs URL as `.docs`; graph.recipes() lists the
|
|
480
|
+
// whole catalog (keyless, no network). graph.query() is the raw escape hatch.
|
|
475
481
|
|
|
476
482
|
function graphUrl() {
|
|
477
483
|
// Defaults to the control endpoint, which already IS the graph endpoint. Override for tests
|
|
@@ -497,32 +503,37 @@ function normalizeGraph(body) {
|
|
|
497
503
|
return { columns, rows, statistics: body.statistics || null };
|
|
498
504
|
}
|
|
499
505
|
|
|
500
|
-
// The one place that talks to the graph.
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
506
|
+
// The one place that talks to the graph. POSTs {query,parameters}, and on an HTTP>=400 or
|
|
507
|
+
// an RFC-7807 problem body throws a WhisperError carrying the server's helpful detail
|
|
508
|
+
// (plus .status/.retryAfter). Two-tier auth (Postel: liberal in what we accept): a keyless
|
|
509
|
+
// method (`keyless: true`) NEVER throws for a missing key - with a key it runs unlimited,
|
|
510
|
+
// without one the X-API-Key header is omitted entirely and the server serves the
|
|
511
|
+
// rate-limited keyless taste. A keyed method with no key fails fast with a helpful error.
|
|
512
|
+
// The key is only ever sent in X-API-Key: never logged, never on argv, never returned.
|
|
513
|
+
async function callGraph(cypher, params, opts = {}, methodName = "query", keyless = false) {
|
|
514
|
+
const key = resolveKey(opts) || "";
|
|
515
|
+
if (!key && !keyless) {
|
|
507
516
|
throw new WhisperError(
|
|
508
517
|
`\`graph.${methodName}\` needs a Whisper API key: pass { apiKey } or set WHISPER_API_KEY. ` +
|
|
509
|
-
"(
|
|
518
|
+
"(The direct read verbs - assess, identify, explain, origins, and friends - run keyless " +
|
|
519
|
+
"with no key at all; raw Cypher, the flows, and submit are keyed. See graph.recipes().)"
|
|
510
520
|
);
|
|
511
521
|
}
|
|
512
522
|
const url = graphUrl();
|
|
513
523
|
const timeout = opts.timeout || 60000;
|
|
514
524
|
const ctrl = new AbortController();
|
|
515
525
|
const t = setTimeout(() => ctrl.abort(), timeout);
|
|
526
|
+
const headers = {
|
|
527
|
+
"Content-Type": "application/json",
|
|
528
|
+
Accept: "application/json",
|
|
529
|
+
"User-Agent": UA,
|
|
530
|
+
};
|
|
531
|
+
if (key) headers["X-API-Key"] = key; // present => keyed (unlimited); absent => keyless taste
|
|
516
532
|
let res;
|
|
517
533
|
try {
|
|
518
534
|
res = await fetch(url, {
|
|
519
535
|
method: "POST",
|
|
520
|
-
headers
|
|
521
|
-
"Content-Type": "application/json",
|
|
522
|
-
Accept: "application/json",
|
|
523
|
-
"X-API-Key": key,
|
|
524
|
-
"User-Agent": UA,
|
|
525
|
-
},
|
|
536
|
+
headers,
|
|
526
537
|
body: JSON.stringify({ query: cypher, parameters: params || {} }),
|
|
527
538
|
signal: ctrl.signal,
|
|
528
539
|
});
|
|
@@ -555,39 +566,41 @@ async function callGraph(cypher, params, opts = {}, methodName = "query") {
|
|
|
555
566
|
// The Whisper query catalog, one row per entry (14 direct + 15 flow), in catalog order.
|
|
556
567
|
// A direct row carries its catalog Cypher (input bound as a $-parameter); a flow row
|
|
557
568
|
// carries its gallery/run slug plus the input/param names the workflow runner accepts;
|
|
558
|
-
// the map-call row (submit) enumerates its fields. `
|
|
569
|
+
// the map-call row (submit) enumerates its fields. `keyless` marks the 13 direct read
|
|
570
|
+
// verbs that run with no key at all (two-tier, straight from the catalog's requiresKey);
|
|
571
|
+
// `summary` feeds recipes() discovery; `docs` is the canonical docs URL.
|
|
559
572
|
const GRAPH_DOCS_BASE = "https://www.whisper.security";
|
|
560
573
|
const GRAPH_FLOW_RUN_URL = "https://console.whisper.security/api/gallery/run";
|
|
561
574
|
const GRAPH_METHODS = [
|
|
562
|
-
{ name: "anycastDnsRootSovereignty", mode: "flow", slug: "anycast-dns-root-sovereignty", primary: "country", inputs: ["country"], params: ["instanceType"], docs: GRAPH_DOCS_BASE + "/docs/recipes/compliance" },
|
|
563
|
-
{ name: "attackPath", mode: "flow", slug: "attack-path", primary: "value", inputs: ["value", "other"], params: ["level"], docs: GRAPH_DOCS_BASE + "/docs/recipes/attack-path" },
|
|
564
|
-
{ name: "attackSurface", mode: "flow", slug: "attack-surface", primary: "domain", inputs: ["domain"], params: ["level"], docs: GRAPH_DOCS_BASE + "/docs/recipes/pentest-recon" },
|
|
565
|
-
{ name: "bgpHijackExposure", mode: "flow", slug: "bgp-hijack-exposure", primary: "value", inputs: ["value"], params: [], docs: GRAPH_DOCS_BASE + "/docs/recipes/bgp-routing" },
|
|
566
|
-
{ name: "blastRadius", mode: "flow", slug: "blast-radius", primary: "indicator", inputs: ["indicator"], params: ["depth"], docs: GRAPH_DOCS_BASE + "/docs/recipes/soc" },
|
|
567
|
-
{ name: "buildTakedownEvidencePackage", mode: "flow", slug: "build-takedown-evidence-package", primary: "domain", inputs: ["domain"], params: [], docs: GRAPH_DOCS_BASE + "/docs/recipes/threat-intel" },
|
|
568
|
-
{ name: "discoverAiAgentInfrastructure", mode: "flow", slug: "discover-ai-agent-infrastructure", primary: "value", inputs: ["value"], params: [], docs: GRAPH_DOCS_BASE + "/docs/recipes/pentest-recon" },
|
|
569
|
-
{ name: "indicator", mode: "flow", slug: "indicator", primary: "indicator", inputs: ["indicator"], params: [], docs: GRAPH_DOCS_BASE + "/docs/recipes/soc" },
|
|
570
|
-
{ name: "indicatorEnrichment", mode: "flow", slug: "indicator-enrichment", primary: "value", inputs: ["value"], params: [], docs: GRAPH_DOCS_BASE + "/docs/recipes/dns-email" },
|
|
571
|
-
{ name: "infrastructureMapping", mode: "flow", slug: "infrastructure-mapping", primary: "value", inputs: ["value"], params: ["level"], docs: GRAPH_DOCS_BASE + "/docs/recipes/compliance" },
|
|
572
|
-
{ name: "mapSupplyChainConcentration", mode: "flow", slug: "map-supply-chain-concentration", primary: "domain", inputs: ["domain"], params: [], docs: GRAPH_DOCS_BASE + "/docs/recipes/compliance" },
|
|
573
|
-
{ name: "nameserverHijackDnsConsistency", mode: "flow", slug: "nameserver-hijack-dns-consistency", primary: "value", inputs: ["value"], params: [], docs: GRAPH_DOCS_BASE + "/docs/recipes/dns-email" },
|
|
574
|
-
{ name: "routeHealth", mode: "flow", slug: "route-health", primary: "target", inputs: ["target"], params: [], docs: GRAPH_DOCS_BASE + "/docs/recipes/bgp-routing" },
|
|
575
|
-
{ name: "subdomainTakeover", mode: "flow", slug: "subdomain-takeover", primary: "value", inputs: ["value"], params: [], docs: GRAPH_DOCS_BASE + "/docs/recipes/pentest-recon" },
|
|
576
|
-
{ name: "typosquat", mode: "flow", slug: "typosquat", primary: "domain", inputs: ["domain"], params: [], docs: GRAPH_DOCS_BASE + "/docs/recipes/brand-protection" },
|
|
577
|
-
{ name: "identify", mode: "direct", input: "v", cypher: "CALL whisper.identify([$v]) YIELD host, vendor_id, canonical_name, category, roles, host_class, band", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/identify" },
|
|
578
|
-
{ name: "assess", mode: "direct", input: "v", cypher: "CALL whisper.assess([$v]) YIELD host, label, band, sub_labels, coverage, evidence", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures" },
|
|
579
|
-
{ name: "variants", mode: "direct", input: "v", cypher: "CALL whisper.variants($v) YIELD variant, method, exists, confidence", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/variants" },
|
|
580
|
-
{ name: "walk", mode: "direct", input: "v", cypher: "CALL whisper.walk($v) YIELD coverage, host, nearest_known_vendors, no_atlas_match, siblings", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures" },
|
|
581
|
-
{ name: "explain", mode: "direct", input: "v", cypher: "CALL whisper.explain($v) YIELD indicator, score, level, explanation, sources", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/explain" },
|
|
582
|
-
{ name: "pslTldplusone", mode: "direct", input: "v", cypher: "CALL whisper.psl.tldPlusOne($v) YIELD apex", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/helpers" },
|
|
583
|
-
{ name: "pslAffiliation", mode: "direct", input: "v", cypher: "CALL whisper.psl.affiliation($v) YIELD found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/helpers" },
|
|
584
|
-
{ name: "origins", mode: "direct", input: "v", cypher: "CALL whisper.origins($v) YIELD ip, confidence, methods, asn, asnName, kind", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/origins" },
|
|
585
|
-
{ name: "history", mode: "direct", input: "v", cypher: "CALL whisper.history($v)", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/history" },
|
|
586
|
-
{ name: "historyWhois", mode: "direct", input: "v", cypher: "CALL whisper.history.whois($v) YIELD queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/history" },
|
|
587
|
-
{ name: "asset", mode: "direct", input: "v", cypher: "CALL whisper.asSet($v) YIELD asSetName, memberAsn, sourceRir", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures" },
|
|
588
|
-
{ name: "lookupTorRelay", mode: "direct", input: "v", cypher: "CALL whisper.lookupTorRelay($v) YIELD indicator, found, fingerprint, exitAddressCount, source, ingestedAt", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/helpers" },
|
|
589
|
-
{ name: "dbSchema", mode: "direct", input: null, cypher: "CALL db.schema()", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/schema" },
|
|
590
|
-
{ name: "submit", mode: "direct", proc: "whisper.submit", fields: [["kind", "kind"], ["identifierKind", "identifier_kind"], ["value", "value"], ["observationId", "observation_id"], ["confidence", "confidence"], ["firstSeen", "first_seen"], ["provenance", "provenance"], ["query", "query"], ["results", "results"], ["comment", "comment"], ["severity", "severity"], ["v", "v"]], requires: ["kind"], docs: GRAPH_DOCS_BASE + "/docs/cypher-api" },
|
|
575
|
+
{ name: "anycastDnsRootSovereignty", mode: "flow", keyless: false, slug: "anycast-dns-root-sovereignty", primary: "country", inputs: ["country"], params: ["instanceType"], summary: "Assess how resilient a country's core DNS is if it were cut off from the world.", docs: GRAPH_DOCS_BASE + "/docs/recipes/compliance" },
|
|
576
|
+
{ name: "attackPath", mode: "flow", keyless: false, slug: "attack-path", primary: "value", inputs: ["value", "other"], params: ["level"], summary: "Find the choke points an attacker would target - and how any two things connect.", docs: GRAPH_DOCS_BASE + "/docs/recipes/attack-path" },
|
|
577
|
+
{ name: "attackSurface", mode: "flow", keyless: false, slug: "attack-surface", primary: "domain", inputs: ["domain"], params: ["level"], summary: "Map everything about a domain that's exposed to the outside world, scored for risk.", docs: GRAPH_DOCS_BASE + "/docs/recipes/pentest-recon" },
|
|
578
|
+
{ name: "bgpHijackExposure", mode: "flow", keyless: false, slug: "bgp-hijack-exposure", primary: "value", inputs: ["value"], params: [], summary: "Grade a network's routing security and trace conflicts to the domains they'd expose.", docs: GRAPH_DOCS_BASE + "/docs/recipes/bgp-routing" },
|
|
579
|
+
{ name: "blastRadius", mode: "flow", keyless: false, slug: "blast-radius", primary: "indicator", inputs: ["indicator"], params: ["depth"], summary: "Pick one asset and see what would break if it failed - and what it depends on in turn.", docs: GRAPH_DOCS_BASE + "/docs/recipes/soc" },
|
|
580
|
+
{ name: "buildTakedownEvidencePackage", mode: "flow", keyless: false, slug: "build-takedown-evidence-package", primary: "domain", inputs: ["domain"], params: [], summary: "Assemble a ready-to-submit dossier for taking down a scam or phishing domain.", docs: GRAPH_DOCS_BASE + "/docs/recipes/threat-intel" },
|
|
581
|
+
{ name: "discoverAiAgentInfrastructure", mode: "flow", keyless: false, slug: "discover-ai-agent-infrastructure", primary: "value", inputs: ["value"], params: [], summary: "Map an organisation's externally visible AI and agent endpoints from the outside.", docs: GRAPH_DOCS_BASE + "/docs/recipes/pentest-recon" },
|
|
582
|
+
{ name: "indicator", mode: "flow", keyless: false, slug: "indicator", primary: "indicator", inputs: ["indicator"], params: [], summary: "Investigate one suspicious domain, IP, or network in depth and get a clear picture of the threat and everything connected to it.", docs: GRAPH_DOCS_BASE + "/docs/recipes/soc" },
|
|
583
|
+
{ name: "indicatorEnrichment", mode: "flow", keyless: false, slug: "indicator-enrichment", primary: "value", inputs: ["value"], params: [], summary: "Turn one domain or IP into a full context card - owner, hosting, mail, location, reputation at a glance.", docs: GRAPH_DOCS_BASE + "/docs/recipes/dns-email" },
|
|
584
|
+
{ name: "infrastructureMapping", mode: "flow", keyless: false, slug: "infrastructure-mapping", primary: "value", inputs: ["value"], params: ["level"], summary: "Trace one indicator to its true owner and full estate, even behind privacy screens and CDNs.", docs: GRAPH_DOCS_BASE + "/docs/recipes/compliance" },
|
|
585
|
+
{ name: "mapSupplyChainConcentration", mode: "flow", keyless: false, slug: "map-supply-chain-concentration", primary: "domain", inputs: ["domain"], params: [], summary: "Grade an organisation for over-reliance on single providers, regions, or facilities.", docs: GRAPH_DOCS_BASE + "/docs/recipes/compliance" },
|
|
586
|
+
{ name: "nameserverHijackDnsConsistency", mode: "flow", keyless: false, slug: "nameserver-hijack-dns-consistency", primary: "value", inputs: ["value"], params: [], summary: "Check a domain's name servers for the misconfigurations that enable DNS hijacking.", docs: GRAPH_DOCS_BASE + "/docs/recipes/dns-email" },
|
|
587
|
+
{ name: "routeHealth", mode: "flow", keyless: false, slug: "route-health", primary: "target", inputs: ["target"], params: [], summary: "Profile a network or address block into a full routing and reachability health card.", docs: GRAPH_DOCS_BASE + "/docs/recipes/bgp-routing" },
|
|
588
|
+
{ name: "subdomainTakeover", mode: "flow", keyless: false, slug: "subdomain-takeover", primary: "value", inputs: ["value"], params: [], summary: "Find subdomains that point at abandoned services an attacker could claim.", docs: GRAPH_DOCS_BASE + "/docs/recipes/pentest-recon" },
|
|
589
|
+
{ name: "typosquat", mode: "flow", keyless: false, slug: "typosquat", primary: "domain", inputs: ["domain"], params: [], summary: "Find registered look-alikes of your brand and check which ones are dangerous.", docs: GRAPH_DOCS_BASE + "/docs/recipes/brand-protection" },
|
|
590
|
+
{ name: "identify", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.identify([$v]) YIELD host, vendor_id, canonical_name, category, roles, host_class, band", summary: "Name the vendor and operator role behind a host or IP in one call.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/identify" },
|
|
591
|
+
{ name: "assess", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.assess([$v]) YIELD host, label, band, sub_labels, coverage, evidence", summary: "Get a labelled threat posture for a host or IP - malicious, benign, or unknown.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures" },
|
|
592
|
+
{ name: "variants", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.variants($v) YIELD variant, method, exists, confidence", summary: "Generate look-alike domain variants of a brand and see which are registered.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/variants" },
|
|
593
|
+
{ name: "walk", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.walk($v) YIELD coverage, host, nearest_known_vendors, no_atlas_match, siblings", summary: "Walk the graph to the nearest known vendors behind a host, with the channel and confidence.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures" },
|
|
594
|
+
{ name: "explain", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.explain($v) YIELD indicator, score, level, explanation, sources", summary: "Score an indicator against the threat feeds and explain exactly why.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/explain" },
|
|
595
|
+
{ name: "pslTldplusone", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.psl.tldPlusOne($v) YIELD apex", summary: "Reduce any hostname to its registrable apex (eTLD+1) via the Public Suffix List.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/helpers" },
|
|
596
|
+
{ name: "pslAffiliation", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.psl.affiliation($v) YIELD found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence", summary: "Check whether a domain is a PSL private-section suffix and who submitted it.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/helpers" },
|
|
597
|
+
{ name: "origins", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.origins($v) YIELD ip, confidence, methods, asn, asnName, kind", summary: "Find the real origin IPs behind a CDN-fronted domain, ranked by confidence.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/origins" },
|
|
598
|
+
{ name: "history", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.history($v)", summary: "Get the full historical WHOIS timeline for a domain.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/history" },
|
|
599
|
+
{ name: "historyWhois", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.history.whois($v) YIELD queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers", summary: "Get the WHOIS-only historical timeline for a domain.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/history" },
|
|
600
|
+
{ name: "asset", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.asSet($v) YIELD asSetName, memberAsn, sourceRir", summary: "List the member ASNs of an AS-SET macro.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures" },
|
|
601
|
+
{ name: "lookupTorRelay", mode: "direct", keyless: true, input: "v", cypher: "CALL whisper.lookupTorRelay($v) YIELD indicator, found, fingerprint, exitAddressCount, source, ingestedAt", summary: "Check whether an IP is a known Tor exit relay.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/procedures/helpers" },
|
|
602
|
+
{ name: "dbSchema", mode: "direct", keyless: true, input: null, cypher: "CALL db.schema()", summary: "List every node and relationship type in the graph with counts and examples.", docs: GRAPH_DOCS_BASE + "/docs/whisper-graph/schema" },
|
|
603
|
+
{ name: "submit", mode: "direct", keyless: false, proc: "whisper.submit", fields: [["kind", "kind"], ["identifierKind", "identifier_kind"], ["value", "value"], ["observationId", "observation_id"], ["confidence", "confidence"], ["firstSeen", "first_seen"], ["provenance", "provenance"], ["query", "query"], ["results", "results"], ["comment", "comment"], ["severity", "severity"], ["v", "v"]], requires: ["kind"], summary: "Contribute an indicator observation or feedback back into the graph (requires an API key).", docs: GRAPH_DOCS_BASE + "/docs/cypher-api" },
|
|
591
604
|
];
|
|
592
605
|
// END GENERATED: graph catalog
|
|
593
606
|
|
|
@@ -616,9 +629,28 @@ async function graphQuery(cypher, params, opts = {}) {
|
|
|
616
629
|
return callGraph(String(cypher), params || {}, opts, "query");
|
|
617
630
|
}
|
|
618
631
|
|
|
632
|
+
/**
|
|
633
|
+
* Discover the whole catalog: every graph method with `{ method, keyless, mode, summary,
|
|
634
|
+
* params, docsUrl }` - baked from the Whisper query catalog at generation time, so it
|
|
635
|
+
* needs no key and no network. `keyless: true` marks the direct read verbs that run with
|
|
636
|
+
* no key at all; `params` are the method's input names (a flow's inputs, a direct verb's
|
|
637
|
+
* $-parameter, the submit fields). Call any method by name, or check its docsUrl.
|
|
638
|
+
* @see https://www.whisper.security/docs/recipes
|
|
639
|
+
*/
|
|
640
|
+
function graphRecipes() {
|
|
641
|
+
return GRAPH_METHODS.map((m) => ({
|
|
642
|
+
method: m.name,
|
|
643
|
+
keyless: !!m.keyless,
|
|
644
|
+
mode: m.mode,
|
|
645
|
+
summary: m.summary,
|
|
646
|
+
params: m.mode === "flow" ? m.inputs.slice() : m.fields ? m.fields.map(([, param]) => param) : m.input ? [m.input] : [],
|
|
647
|
+
docsUrl: m.docs,
|
|
648
|
+
}));
|
|
649
|
+
}
|
|
650
|
+
|
|
619
651
|
// ── Flow runner (gallery/run, Server-Sent Events) ───────────────────────────────────
|
|
620
652
|
// A flow entry is a multi-step read workflow orchestrated by the Whisper workflow runner:
|
|
621
|
-
// POST {slug,
|
|
653
|
+
// POST {slug, value, paramValues} to the gallery/run endpoint (keyed, X-API-Key) and consume
|
|
622
654
|
// the SSE stream. Each `step` event carries a step's bound Cypher + rows; the terminal
|
|
623
655
|
// `complete` event carries the aggregate context. callFlow() collects the whole stream and
|
|
624
656
|
// resolves to { slug, steps, complete, events } - one call, the full flow.
|
|
@@ -652,6 +684,36 @@ function flowBody(m, input) {
|
|
|
652
684
|
return { inputs, params };
|
|
653
685
|
}
|
|
654
686
|
|
|
687
|
+
// Map the routed { inputs, params } onto the runner's wire contract {slug, value, paramValues}:
|
|
688
|
+
// `value` is the ONE primary entity (a host / IP / ASN), or `values` for a bulk list; the FIRST
|
|
689
|
+
// input becomes the anchor `value`, and every other input plus every tuning knob rides in
|
|
690
|
+
// `paramValues`. We send ONLY the keys the runner reads - an `inputs`/`params` map is silently
|
|
691
|
+
// ignored, so the flow would fall back to its default anchor. A nullish input is skipped so the
|
|
692
|
+
// flow's own default applies (Postel: sensible defaults, liberal in what we accept).
|
|
693
|
+
function flowWireBody(m, input) {
|
|
694
|
+
const { inputs, params } = flowBody(m, input);
|
|
695
|
+
const paramValues = {};
|
|
696
|
+
let value;
|
|
697
|
+
let values;
|
|
698
|
+
let first = true;
|
|
699
|
+
for (const [name, val] of Object.entries(inputs)) {
|
|
700
|
+
if (val === undefined || val === null) continue;
|
|
701
|
+
if (first) {
|
|
702
|
+
if (Array.isArray(val)) values = val.map((x) => String(x));
|
|
703
|
+
else value = String(val);
|
|
704
|
+
first = false;
|
|
705
|
+
} else {
|
|
706
|
+
paramValues[name] = val;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
for (const [k, v] of Object.entries(params)) paramValues[k] = v;
|
|
710
|
+
const body = { slug: m.slug };
|
|
711
|
+
if (value !== undefined) body.value = value;
|
|
712
|
+
if (values !== undefined) body.values = values;
|
|
713
|
+
if (Object.keys(paramValues).length > 0) body.paramValues = paramValues;
|
|
714
|
+
return body;
|
|
715
|
+
}
|
|
716
|
+
|
|
655
717
|
// Parse one SSE record ("event: x\ndata: {...}") into { event, data }; data is JSON-decoded
|
|
656
718
|
// when possible, else kept as the raw string. Never throws on shape.
|
|
657
719
|
function parseSseRecord(record) {
|
|
@@ -671,7 +733,7 @@ function parseSseRecord(record) {
|
|
|
671
733
|
}
|
|
672
734
|
|
|
673
735
|
// The one place that talks to the workflow runner. Requires a key (shared resolveKey),
|
|
674
|
-
// POSTs {slug,
|
|
736
|
+
// POSTs {slug, value, paramValues}, consumes the SSE stream, and resolves to the collected
|
|
675
737
|
// { slug, steps, complete, events }. Flows are multi-step, so the default timeout is 5
|
|
676
738
|
// minutes (override with opts.timeout). On an HTTP error, an `error` event, or a stream
|
|
677
739
|
// that ends with no result at all, throws a WhisperError with the server's clear detail.
|
|
@@ -680,10 +742,10 @@ async function callFlow(m, input, opts = {}) {
|
|
|
680
742
|
if (!key) {
|
|
681
743
|
throw new WhisperError(
|
|
682
744
|
`\`graph.${m.name}\` needs a Whisper API key: pass { apiKey } or set WHISPER_API_KEY. ` +
|
|
683
|
-
"(
|
|
745
|
+
"(The graph's direct read verbs run keyless; flows run keyed on the workflow runner.)"
|
|
684
746
|
);
|
|
685
747
|
}
|
|
686
|
-
const
|
|
748
|
+
const body = flowWireBody(m, input);
|
|
687
749
|
const url = flowRunUrl();
|
|
688
750
|
const timeout = opts.timeout || 300000;
|
|
689
751
|
const ctrl = new AbortController();
|
|
@@ -699,7 +761,7 @@ async function callFlow(m, input, opts = {}) {
|
|
|
699
761
|
"X-API-Key": key,
|
|
700
762
|
"User-Agent": UA,
|
|
701
763
|
},
|
|
702
|
-
body: JSON.stringify(
|
|
764
|
+
body: JSON.stringify(body),
|
|
703
765
|
signal: ctrl.signal,
|
|
704
766
|
});
|
|
705
767
|
} catch (err) {
|
|
@@ -810,12 +872,13 @@ function makeMapCall(m) {
|
|
|
810
872
|
};
|
|
811
873
|
}
|
|
812
874
|
|
|
813
|
-
// Build the namespace from the generated catalog table: query + one method per
|
|
814
|
-
// Direct methods run their catalog Cypher
|
|
815
|
-
//
|
|
816
|
-
// URL as `.docs` (docsBase +
|
|
817
|
-
const graph = { query: graphQuery };
|
|
875
|
+
// Build the namespace from the generated catalog table: query + recipes + one method per
|
|
876
|
+
// entry. Direct methods run their catalog Cypher (the 13 keyless reads never require a
|
|
877
|
+
// key); the map-call write builds its parameter map; flow methods execute on the workflow
|
|
878
|
+
// runner. Every method carries its canonical docs URL as `.docs` (docsBase + docPath).
|
|
879
|
+
const graph = { query: graphQuery, recipes: graphRecipes };
|
|
818
880
|
graph.query.docs = GRAPH_DOCS_BASE + "/docs/cypher-api";
|
|
881
|
+
graph.recipes.docs = GRAPH_DOCS_BASE + "/docs/recipes";
|
|
819
882
|
|
|
820
883
|
for (const m of GRAPH_METHODS) {
|
|
821
884
|
if (m.mode === "flow") {
|
|
@@ -823,9 +886,9 @@ for (const m of GRAPH_METHODS) {
|
|
|
823
886
|
} else if (m.fields) {
|
|
824
887
|
graph[m.name] = makeMapCall(m);
|
|
825
888
|
} else if (m.input) {
|
|
826
|
-
graph[m.name] = async (value, opts = {}) => callGraph(m.cypher, { [m.input]: graphValue(m.name, value) }, opts, m.name);
|
|
889
|
+
graph[m.name] = async (value, opts = {}) => callGraph(m.cypher, { [m.input]: graphValue(m.name, value) }, opts, m.name, m.keyless);
|
|
827
890
|
} else {
|
|
828
|
-
graph[m.name] = async (opts = {}) => callGraph(m.cypher, {}, opts, m.name);
|
|
891
|
+
graph[m.name] = async (opts = {}) => callGraph(m.cypher, {}, opts, m.name, m.keyless);
|
|
829
892
|
}
|
|
830
893
|
graph[m.name].docs = m.docs;
|
|
831
894
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whisper-id",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Give any Node agent the Whisper security graph (keyless reads + keyed Cypher), a real routable Whisper IPv6 identity, safe egress, and the full control plane.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|