whisper-id 0.6.0 → 0.7.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 +10 -9
- package/SECURITY.md +26 -0
- package/index.d.ts +43 -39
- package/index.js +31 -36
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# whisper-id
|
|
2
2
|
|
|
3
|
-
Three things for any Node agent, in one dependency-free package: query the **Whisper security graph** (
|
|
3
|
+
Three things for any Node agent, in one dependency-free package: query the **Whisper security graph** (7.48B nodes and 39.5B edges joining the physical, network, naming, ownership and threat layers; live totals at nic.whisper.online/stats, Cypher), give the agent a routable **IPv6 identity** with safe **egress**, and drive the full **control plane**.
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm i whisper-id
|
|
@@ -8,7 +8,7 @@ npm i whisper-id
|
|
|
8
8
|
|
|
9
9
|
## The security graph (keyless, zero setup)
|
|
10
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
|
|
11
|
+
The Whisper graph knows who operates a host, its threat posture, its look-alikes, the real origins behind a CDN, WHOIS history, and 15 named investigations. The **direct read verbs run with no key at all** (rate-limited taste, ~100/window). One import, real answers:
|
|
12
12
|
|
|
13
13
|
```js
|
|
14
14
|
import { graph } from "whisper-id"; // no key needed for the read verbs
|
|
@@ -19,7 +19,7 @@ await graph.origins("cloudflare.com"); // the real origin IPs behind
|
|
|
19
19
|
await graph.explain("paypal.com"); // threat-feed score + why
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Set `WHISPER_API_KEY` (or pass `{ apiKey }`) to lift the rate limit
|
|
22
|
+
Set `WHISPER_API_KEY` (or pass `{ apiKey }`) to lift the keyless rate limit. Every verb is sent either way; the graph decides what to answer:
|
|
23
23
|
|
|
24
24
|
```js
|
|
25
25
|
// raw Cypher, your own query, parameters bound as $-params (never string-built):
|
|
@@ -35,7 +35,7 @@ for (const r of graph.recipes()) {
|
|
|
35
35
|
}
|
|
36
36
|
```
|
|
37
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
|
|
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 answer keyless; the 15 multi-step flows and the `submit` write channel are proxied up the same way and the graph decides. Full query reference: [whisper-catalog](https://github.com/whisper-sec/whisper-catalog).
|
|
39
39
|
|
|
40
40
|
## Identity + egress
|
|
41
41
|
|
|
@@ -78,12 +78,12 @@ if (await verify(addr)) console.log((await rdap(addr)).name); // keyless, pure
|
|
|
78
78
|
| `graph.asset(value)` | yes | Member ASNs of an AS-SET macro |
|
|
79
79
|
| `graph.lookupTorRelay(value)` | yes | Is an IP a known Tor exit relay |
|
|
80
80
|
| `graph.dbSchema()` | yes | Every node + relationship type in the graph, with counts and examples |
|
|
81
|
-
| `graph.query(cypher, params)` |
|
|
81
|
+
| `graph.query(cypher, params)` | yes | Raw Cypher escape hatch. -> the full `{ columns, rows, statistics }` |
|
|
82
82
|
| `graph.submit({ kind, ...fields })` | key | Contribute an indicator observation or feedback (the write channel) |
|
|
83
83
|
| `graph.recipes()` | yes (offline) | The whole catalog: `{ method, keyless, mode, summary, params, docsUrl }` per verb |
|
|
84
84
|
|
|
85
85
|
Fifteen multi-step **workflow** methods (`attackPath`, `attackSurface`, `blastRadius`, `typosquat`,
|
|
86
|
-
`indicator`, `routeHealth`, and 9 more) execute
|
|
86
|
+
`indicator`, `routeHealth`, and 9 more) execute on the Whisper workflow runner and stream
|
|
87
87
|
Server-Sent Events; one call collects the whole run into `{ slug, steps, complete, events }`:
|
|
88
88
|
|
|
89
89
|
```js
|
|
@@ -165,14 +165,15 @@ transparently inside `withEgress`.
|
|
|
165
165
|
|
|
166
166
|
Zero dependencies. The **graph read verbs**, the **keyless** checks, and the **control plane** are
|
|
167
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
|
|
169
|
-
|
|
168
|
+
(or pass `{ apiKey }`) for the control plane (`whisper.agents`), which always needs it. Graph calls
|
|
169
|
+
never require a key from this SDK: they go up with one or without, and the graph decides what to
|
|
170
|
+
answer. A key lifts the keyless rate limit. `verify` and `rdap` need no key either.
|
|
170
171
|
|
|
171
172
|
Only **egress** and the CLI-backed calls (`register`, `egress`, `withEgress`, `ip`) need the
|
|
172
173
|
`whisper` CLI on your `PATH`:
|
|
173
174
|
|
|
174
175
|
```sh
|
|
175
|
-
curl get.whisper.online | sh
|
|
176
|
+
curl -fsSL https://get.whisper.online | sh
|
|
176
177
|
```
|
|
177
178
|
|
|
178
179
|
(`$WHISPER_BIN` overrides the CLI path; `$WHISPER_CONTROL_URL` / `$WHISPER_GRAPH_URL` /
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please report security issues privately to **security@whisper.security**. Do not
|
|
6
|
+
open a public issue for a suspected vulnerability.
|
|
7
|
+
|
|
8
|
+
Include what you found, how to reproduce it, and the impact you expect. We aim to
|
|
9
|
+
acknowledge within a few business days and will keep you updated as we work on a
|
|
10
|
+
fix. Coordinated disclosure is appreciated.
|
|
11
|
+
|
|
12
|
+
## Scope
|
|
13
|
+
|
|
14
|
+
This repository is the **Node client** for Whisper: the control plane
|
|
15
|
+
(`whisper.agents`), the keyless public checks, and the Whisper security graph. It
|
|
16
|
+
talks only to Whisper's public endpoints and holds no server-side secrets.
|
|
17
|
+
|
|
18
|
+
Your API key travels in one place and one place only: the `X-API-Key` request
|
|
19
|
+
header. It is never written to a URL, a request body, a log line, or argv. A call
|
|
20
|
+
made without a key sends no auth header at all rather than an empty one. Any
|
|
21
|
+
deviation from that is a security bug and is in scope, as are the input validation
|
|
22
|
+
and the redaction of keys from error text.
|
|
23
|
+
|
|
24
|
+
## Supported versions
|
|
25
|
+
|
|
26
|
+
The latest published release receives security fixes. Older versions may not.
|
package/index.d.ts
CHANGED
|
@@ -157,7 +157,7 @@ export function agent(opts?: AgentSelectOptions): Promise<AgentItem | null>;
|
|
|
157
157
|
// KEYLESS - no key at all, a rate-limited taste with real answers; a key ({ apiKey } or
|
|
158
158
|
// WHISPER_API_KEY) lifts the limit. Raw Cypher (`graph.query`), the 15 multi-step `flow`
|
|
159
159
|
// methods (workflow runner: POST gallery/run, Server-Sent Events), and the `submit`
|
|
160
|
-
// write
|
|
160
|
+
// write are proxied up the same way. A present key rides only in `X-API-Key`; an absent one sends no auth
|
|
161
161
|
// header at all. The method surface below is GENERATED from the Whisper query catalog by
|
|
162
162
|
// scripts/gen-graph.mjs; every method's docs live at the `@see` URL and are also on the
|
|
163
163
|
// function itself as `.docs`, and `graph.recipes()` lists the whole catalog.
|
|
@@ -181,7 +181,7 @@ export interface GraphRecipe {
|
|
|
181
181
|
docsUrl: string;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
/** Result of a
|
|
184
|
+
/** Result of a graph query. `rows` are objects keyed by column name. */
|
|
185
185
|
export interface GraphResult {
|
|
186
186
|
columns: string[];
|
|
187
187
|
rows: Record<string, unknown>[];
|
|
@@ -237,15 +237,18 @@ export interface GraphFlowResult {
|
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
|
-
* The graph namespace: one typed method per catalog entry (
|
|
241
|
-
* 15 workflow-runner flows), plus a raw `query` escape hatch and
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
240
|
+
* The graph namespace: one typed method per catalog entry (13 direct Cypher reads,
|
|
241
|
+
* `submit`, and 15 workflow-runner flows), plus a raw `query` escape hatch and
|
|
242
|
+
* `recipes()` discovery. Every verb is proxied to the graph with a key if you have one
|
|
243
|
+
* and without if you do not: the graph owns the keyless policy and decides what to
|
|
244
|
+
* answer, so the SDK never refuses a call on your behalf. A key (`{ apiKey }` or
|
|
245
|
+
* `WHISPER_API_KEY`) lifts the keyless rate limit. The control plane (`whisper.agents`:
|
|
246
|
+
* agents, policy, logs, identity) is a separate surface and always requires a key.
|
|
247
|
+
* Every method carries its canonical docs URL as `.docs`.
|
|
245
248
|
*/
|
|
246
249
|
export interface GraphNamespace {
|
|
247
250
|
/**
|
|
248
|
-
* Raw Cypher escape hatch
|
|
251
|
+
* Raw Cypher escape hatch: run arbitrary Cypher with `params` bound
|
|
249
252
|
* server-side as $-parameters. Returns the full `{columns,rows,statistics}`.
|
|
250
253
|
* @see https://www.whisper.security/docs/cypher-api
|
|
251
254
|
*/
|
|
@@ -268,7 +271,7 @@ export interface GraphNamespace {
|
|
|
268
271
|
* letters have in-country DNS_ROOT_INSTANCE anycast nodes, the Global-vs-Local split, the BGP
|
|
269
272
|
* origin ASNs hosting them, and a resilience grade.
|
|
270
273
|
*
|
|
271
|
-
*
|
|
274
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "anycast-dns-root-sovereignty")
|
|
272
275
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
273
276
|
* Inputs: country. Params: instanceType.
|
|
274
277
|
* @see https://www.whisper.security/docs/recipes/compliance
|
|
@@ -281,7 +284,7 @@ export interface GraphNamespace {
|
|
|
281
284
|
* From a starting foothold, finds shared dependencies whose compromise reaches furthest; for
|
|
282
285
|
* any two indicators, traces how they are actually connected.
|
|
283
286
|
*
|
|
284
|
-
*
|
|
287
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "attack-path")
|
|
285
288
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
286
289
|
* Inputs: value, other. Params: level.
|
|
287
290
|
* @see https://www.whisper.security/docs/recipes/attack-path
|
|
@@ -294,7 +297,7 @@ export interface GraphNamespace {
|
|
|
294
297
|
* Maps the full external footprint - subdomains, name/mail servers, registrant, third-party
|
|
295
298
|
* services, connected web - and scores the exposure.
|
|
296
299
|
*
|
|
297
|
-
*
|
|
300
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "attack-surface")
|
|
298
301
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
299
302
|
* Inputs: domain. Params: level.
|
|
300
303
|
* @see https://www.whisper.security/docs/recipes/pentest-recon
|
|
@@ -307,7 +310,7 @@ export interface GraphNamespace {
|
|
|
307
310
|
* Grades a network on the conflicts/gaps that make route hijacking possible, then traces any
|
|
308
311
|
* conflict to the specific domains and organisations exposed on the affected blocks.
|
|
309
312
|
*
|
|
310
|
-
*
|
|
313
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "bgp-hijack-exposure")
|
|
311
314
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
312
315
|
* Inputs: value.
|
|
313
316
|
* @see https://www.whisper.security/docs/recipes/bgp-routing
|
|
@@ -320,7 +323,7 @@ export interface GraphNamespace {
|
|
|
320
323
|
* Maps dependencies in both directions: everything that breaks if the asset fails (SPOFs) and
|
|
321
324
|
* everything it relies on (its own DNS/mail/hosting/network supply chain).
|
|
322
325
|
*
|
|
323
|
-
*
|
|
326
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "blast-radius")
|
|
324
327
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
325
328
|
* Inputs: indicator. Params: depth.
|
|
326
329
|
* @see https://www.whisper.security/docs/recipes/soc
|
|
@@ -333,7 +336,7 @@ export interface GraphNamespace {
|
|
|
333
336
|
* One-pass takedown package: reputation verdict, owner (WHOIS), abuse-list listings, and
|
|
334
337
|
* surrounding infrastructure, laid out ready to hand to a registrar/host.
|
|
335
338
|
*
|
|
336
|
-
*
|
|
339
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "build-takedown-evidence-package")
|
|
337
340
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
338
341
|
* Inputs: domain.
|
|
339
342
|
* @see https://www.whisper.security/docs/recipes/threat-intel
|
|
@@ -346,7 +349,7 @@ export interface GraphNamespace {
|
|
|
346
349
|
* Maps externally visible AI/agent hosts (API, model, agent) from the outside via heuristic
|
|
347
350
|
* hostname patterns (api./mcp./ai./vector./llm./agent./chat./copilot.). Best-effort leads.
|
|
348
351
|
*
|
|
349
|
-
*
|
|
352
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "discover-ai-agent-infrastructure")
|
|
350
353
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
351
354
|
* Inputs: value.
|
|
352
355
|
* @see https://www.whisper.security/docs/recipes/pentest-recon
|
|
@@ -360,7 +363,7 @@ export interface GraphNamespace {
|
|
|
360
363
|
* CDN, neighbouring infra), threat-checks each node, never inherits maliciousness from shared
|
|
361
364
|
* infra; labelled posture, no score.
|
|
362
365
|
*
|
|
363
|
-
*
|
|
366
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "indicator")
|
|
364
367
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
365
368
|
* Inputs: indicator.
|
|
366
369
|
* @see https://www.whisper.security/docs/recipes/soc
|
|
@@ -373,7 +376,7 @@ export interface GraphNamespace {
|
|
|
373
376
|
* Fills the picture for one indicator: registrant (WHOIS), hosting + country, mail/name
|
|
374
377
|
* servers, network behind it, reputation read.
|
|
375
378
|
*
|
|
376
|
-
*
|
|
379
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "indicator-enrichment")
|
|
377
380
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
378
381
|
* Inputs: value.
|
|
379
382
|
* @see https://www.whisper.security/docs/recipes/dns-email
|
|
@@ -386,7 +389,7 @@ export interface GraphNamespace {
|
|
|
386
389
|
* Works out the true operator (even behind privacy WHOIS), de-cloaks CDN-fronted sites to real
|
|
387
390
|
* servers, pivots to the rest of that owner's estate across every layer.
|
|
388
391
|
*
|
|
389
|
-
*
|
|
392
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "infrastructure-mapping")
|
|
390
393
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
391
394
|
* Inputs: value. Params: level.
|
|
392
395
|
* @see https://www.whisper.security/docs/recipes/compliance
|
|
@@ -399,7 +402,7 @@ export interface GraphNamespace {
|
|
|
399
402
|
* Grades how concentrated infra is - too much riding on one provider/region/data-centre/cable
|
|
400
403
|
* landing - surfacing SPOFs for resilience and DORA/NIS2 fourth-party risk.
|
|
401
404
|
*
|
|
402
|
-
*
|
|
405
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "map-supply-chain-concentration")
|
|
403
406
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
404
407
|
* Inputs: domain.
|
|
405
408
|
* @see https://www.whisper.security/docs/recipes/compliance
|
|
@@ -412,7 +415,7 @@ export interface GraphNamespace {
|
|
|
412
415
|
* Audits delegation, flags stale/mismatched/lame nameservers, sizes each provider's share,
|
|
413
416
|
* surfaces registry facts - catches delegation weakness before exploit.
|
|
414
417
|
*
|
|
415
|
-
*
|
|
418
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "nameserver-hijack-dns-consistency")
|
|
416
419
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
417
420
|
* Inputs: value.
|
|
418
421
|
* @see https://www.whisper.security/docs/recipes/dns-email
|
|
@@ -425,7 +428,7 @@ export interface GraphNamespace {
|
|
|
425
428
|
* Health card for a network/block: what it announces, peers/transit, single-upstream lean,
|
|
426
429
|
* RPKI protection - the one-look reach & resilience report.
|
|
427
430
|
*
|
|
428
|
-
*
|
|
431
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "route-health")
|
|
429
432
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
430
433
|
* Inputs: target.
|
|
431
434
|
* @see https://www.whisper.security/docs/recipes/bgp-routing
|
|
@@ -439,7 +442,7 @@ export interface GraphNamespace {
|
|
|
439
442
|
* no longer resolves) so you can reclaim/remove before someone else does. CNAME layer is
|
|
440
443
|
* prod-ahead.
|
|
441
444
|
*
|
|
442
|
-
*
|
|
445
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "subdomain-takeover")
|
|
443
446
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
444
447
|
* Inputs: value.
|
|
445
448
|
* @see https://www.whisper.security/docs/recipes/pentest-recon
|
|
@@ -452,7 +455,7 @@ export interface GraphNamespace {
|
|
|
452
455
|
* Finds registered impersonations across misspellings/risky TLDs, separates your own defensive
|
|
453
456
|
* domains from strangers', flags fresh/privacy/abuse-listed ones into a prioritised list.
|
|
454
457
|
*
|
|
455
|
-
*
|
|
458
|
+
* Flow: streams Server-Sent Events from the workflow runner (slug "typosquat")
|
|
456
459
|
* and resolves to the collected { slug, steps, complete, events }.
|
|
457
460
|
* Inputs: domain.
|
|
458
461
|
* @see https://www.whisper.security/docs/recipes/brand-protection
|
|
@@ -465,7 +468,7 @@ export interface GraphNamespace {
|
|
|
465
468
|
* Resolves a host/IP to its canonical vendor, category and operator roles (DNS_OPERATOR / CDN
|
|
466
469
|
* / ORIGIN_AS / MAIL_RECEIVER) with a confidence band.
|
|
467
470
|
*
|
|
468
|
-
*
|
|
471
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
469
472
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
470
473
|
* Cypher: CALL whisper.identify([$v]) YIELD host, vendor_id, canonical_name, category, roles, host_class, band
|
|
471
474
|
* Returns columns: host, vendor_id, canonical_name, category, roles, host_class, band.
|
|
@@ -479,7 +482,7 @@ export interface GraphNamespace {
|
|
|
479
482
|
* Returns a posture label + severity band + sub-labels + coverage + evidence for an indicator;
|
|
480
483
|
* benign-allowlisted vs malicious-evidenced, never a bare score.
|
|
481
484
|
*
|
|
482
|
-
*
|
|
485
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
483
486
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
484
487
|
* Cypher: CALL whisper.assess([$v]) YIELD host, label, band, sub_labels, coverage, evidence
|
|
485
488
|
* Returns columns: host, label, band, sub_labels, coverage, evidence.
|
|
@@ -493,7 +496,7 @@ export interface GraphNamespace {
|
|
|
493
496
|
* Enumerates permutations (omission, bitsquatting, ...) of a domain, flags which exist, with a
|
|
494
497
|
* per-variant confidence.
|
|
495
498
|
*
|
|
496
|
-
*
|
|
499
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
497
500
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
498
501
|
* Cypher: CALL whisper.variants($v) YIELD variant, method, exists, confidence
|
|
499
502
|
* Returns columns: variant, method, exists, confidence.
|
|
@@ -508,7 +511,7 @@ export interface GraphNamespace {
|
|
|
508
511
|
* ORIGIN_AS) and confidence, plus siblings and coverage - explains WHY a host maps to a
|
|
509
512
|
* vendor.
|
|
510
513
|
*
|
|
511
|
-
*
|
|
514
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
512
515
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
513
516
|
* Cypher: CALL whisper.walk($v) YIELD coverage, host, nearest_known_vendors, no_atlas_match, siblings
|
|
514
517
|
* Returns columns: coverage, host, nearest_known_vendors, no_atlas_match, siblings.
|
|
@@ -523,7 +526,7 @@ export interface GraphNamespace {
|
|
|
523
526
|
* weight, firstSeen/lastSeen). Two live forms: bare CALL explain($v) (full columns) and CALL
|
|
524
527
|
* whisper.explain($v) (restricted YIELD).
|
|
525
528
|
*
|
|
526
|
-
*
|
|
529
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
527
530
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
528
531
|
* Cypher: CALL whisper.explain($v) YIELD indicator, score, level, explanation, sources
|
|
529
532
|
* Returns columns: indicator, score, level, explanation, sources.
|
|
@@ -537,7 +540,7 @@ export interface GraphNamespace {
|
|
|
537
540
|
* PSL-correct eTLD+1: www.foo.co.uk -> foo.co.uk. The right way to group hosts by the thing
|
|
538
541
|
* someone actually registered.
|
|
539
542
|
*
|
|
540
|
-
*
|
|
543
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
541
544
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
542
545
|
* Cypher: CALL whisper.psl.tldPlusOne($v) YIELD apex
|
|
543
546
|
* Returns columns: apex.
|
|
@@ -551,7 +554,7 @@ export interface GraphNamespace {
|
|
|
551
554
|
* For a PSL private-section suffix, returns the submitting org/login and evidence kind +
|
|
552
555
|
* confidence; found=false for ordinary registrable domains.
|
|
553
556
|
*
|
|
554
|
-
*
|
|
557
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
555
558
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
556
559
|
* Cypher: CALL whisper.psl.affiliation($v) YIELD found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence
|
|
557
560
|
* Returns columns: found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence.
|
|
@@ -565,7 +568,7 @@ export interface GraphNamespace {
|
|
|
565
568
|
* Candidate origin IPs behind a CDN with confidence, the methods that found them (e.g.
|
|
566
569
|
* links_to), and the hosting ASN/name - de-cloaks to the real server.
|
|
567
570
|
*
|
|
568
|
-
*
|
|
571
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
569
572
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
570
573
|
* Cypher: CALL whisper.origins($v) YIELD ip, confidence, methods, asn, asnName, kind
|
|
571
574
|
* Returns columns: ip, confidence, methods, asn, asnName, kind.
|
|
@@ -579,7 +582,7 @@ export interface GraphNamespace {
|
|
|
579
582
|
* Every observed WHOIS snapshot over time: create/update/expiry, registrar, registrant,
|
|
580
583
|
* country, nameservers - the ownership/registration story.
|
|
581
584
|
*
|
|
582
|
-
*
|
|
585
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
583
586
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
584
587
|
* Cypher: CALL whisper.history($v)
|
|
585
588
|
* Returns columns: indicator, type, queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers, cached.
|
|
@@ -593,7 +596,7 @@ export interface GraphNamespace {
|
|
|
593
596
|
* The registration-record projection of the history timeline: create/update/expiry, registrar,
|
|
594
597
|
* registrant, country, nameservers per snapshot.
|
|
595
598
|
*
|
|
596
|
-
*
|
|
599
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
597
600
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
598
601
|
* Cypher: CALL whisper.history.whois($v) YIELD queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers
|
|
599
602
|
* Returns columns: queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers.
|
|
@@ -608,7 +611,7 @@ export interface GraphNamespace {
|
|
|
608
611
|
* STRING as-set name, not an integer ASN. (Membership is sparsely populated in the current
|
|
609
612
|
* graph.)
|
|
610
613
|
*
|
|
611
|
-
*
|
|
614
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
612
615
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
613
616
|
* Cypher: CALL whisper.asSet($v) YIELD asSetName, memberAsn, sourceRir
|
|
614
617
|
* Returns columns: asSetName, memberAsn, sourceRir.
|
|
@@ -622,7 +625,7 @@ export interface GraphNamespace {
|
|
|
622
625
|
* Live Tor exit-node check: found + fingerprint + exit-address count + source + ingest time
|
|
623
626
|
* for an IPv4.
|
|
624
627
|
*
|
|
625
|
-
*
|
|
628
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
626
629
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
627
630
|
* Cypher: CALL whisper.lookupTorRelay($v) YIELD indicator, found, fingerprint, exitAddressCount, source, ingestedAt
|
|
628
631
|
* Returns columns: indicator, found, fingerprint, exitAddressCount, source, ingestedAt.
|
|
@@ -636,7 +639,7 @@ export interface GraphNamespace {
|
|
|
636
639
|
* The self-describing schema: node/relationship types with counts, descriptions, examples,
|
|
637
640
|
* source/target labels and query best-practices - the map of what the graph holds.
|
|
638
641
|
*
|
|
639
|
-
*
|
|
642
|
+
* Direct read: answers with NO key at all (a rate-limited taste, real
|
|
640
643
|
* answers); pass a key ({ apiKey } or WHISPER_API_KEY) to lift the limit.
|
|
641
644
|
* Cypher: CALL db.schema()
|
|
642
645
|
* Returns columns: type, name, count, description, example, sourceLabels, targetLabels, fastPatterns, slowPatterns, bestPractices.
|
|
@@ -647,10 +650,11 @@ export interface GraphNamespace {
|
|
|
647
650
|
/**
|
|
648
651
|
* Contribute an indicator observation or feedback back into the graph (requires an API key).
|
|
649
652
|
*
|
|
650
|
-
* The write channel: submit an indicator/feedback with an attributable API key
|
|
651
|
-
*
|
|
653
|
+
* The write channel: submit an indicator/feedback with an attributable API key. The graph
|
|
654
|
+
* requires a key carrying the submit:write scope and answers anonymous callers with a clear
|
|
655
|
+
* 400, to preserve K-anonymity.
|
|
652
656
|
*
|
|
653
|
-
*
|
|
657
|
+
* Write: CALL whisper.submit({...}) with the payload fields bound as $-parameters.
|
|
654
658
|
* Fields: kind, identifier_kind, value, observation_id, confidence, first_seen, provenance, query, results, comment, severity, v (kind required).
|
|
655
659
|
* Returns columns: observation_id, kind, accepted, idempotent, promotion_state, k_bucket, advisory, v.
|
|
656
660
|
* @see https://www.whisper.security/docs/cypher-api
|
package/index.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
// whisper-id - the Whisper security graph, a routable IPv6 identity, and safe egress for
|
|
4
4
|
// any Node agent. Dependency-free and two-tier (Postel): the graph's direct read verbs and
|
|
5
5
|
// the identity checks run KEYLESS over plain fetch() (graph.assess("8.8.8.8") needs no key
|
|
6
|
-
// at all); the
|
|
7
|
-
// register/egress/ip lean on the
|
|
6
|
+
// at all); raw Cypher and the flows are sent the same way and the graph decides. Only the
|
|
7
|
+
// control plane requires WHISPER_API_KEY, and only register/egress/ip lean on the
|
|
8
|
+
// `whisper` CLI (https://whisper.online).
|
|
8
9
|
import { execFile } from "node:child_process";
|
|
9
10
|
import { promisify } from "node:util";
|
|
10
11
|
|
|
@@ -35,7 +36,7 @@ async function run(args, { timeout = 120000, check = true } = {}) {
|
|
|
35
36
|
if (err && err.code === "ENOENT") {
|
|
36
37
|
throw new WhisperError(
|
|
37
38
|
"the `whisper` CLI was not found on PATH. Install it with " +
|
|
38
|
-
"`curl get.whisper.online | sh` (see https://whisper.online)."
|
|
39
|
+
"`curl -fsSL https://get.whisper.online | sh` (see https://whisper.online)."
|
|
39
40
|
);
|
|
40
41
|
}
|
|
41
42
|
const out = { stdout: err.stdout || "", stderr: err.stderr || "", code: typeof err.code === "number" ? err.code : 1 };
|
|
@@ -203,7 +204,7 @@ export async function ip({ timeout = 60000 } = {}) {
|
|
|
203
204
|
// read from opts.apiKey or $WHISPER_API_KEY and sent ONLY in the X-API-Key header.
|
|
204
205
|
|
|
205
206
|
function controlUrl() {
|
|
206
|
-
return process.env.WHISPER_CONTROL_URL || "https://graph.whisper.
|
|
207
|
+
return process.env.WHISPER_CONTROL_URL || "https://graph.whisper.online/api/query";
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
// Conservative-emit (RFC 761): render a value as a Cypher literal so no input - however
|
|
@@ -463,8 +464,8 @@ export async function agent(sel = {}, maybeOpts) {
|
|
|
463
464
|
// origins, history, historyWhois, asset, lookupTorRelay, dbSchema) run KEYLESS - no key
|
|
464
465
|
// at all, a rate-limited taste (~100/window) with real answers; a key (opts.apiKey or
|
|
465
466
|
// WHISPER_API_KEY) lifts the limit. Raw Cypher (graph.query), the 15 multi-step flows,
|
|
466
|
-
// and the submit write channel
|
|
467
|
-
// control plane uses (https://graph.whisper.
|
|
467
|
+
// and the submit write channel are proxied up the same way. Everything POSTs to the SAME endpoint the
|
|
468
|
+
// control plane uses (https://graph.whisper.online/api/query), reusing resolveKey() so
|
|
468
469
|
// there is one auth path, not two: a present key rides ONLY in the X-API-Key header, an
|
|
469
470
|
// absent one sends NO auth header at all. It does NOT go through the whisper.agents()
|
|
470
471
|
// control wrapper: the graph contract is different (body {query,parameters}; reply
|
|
@@ -508,17 +509,12 @@ function normalizeGraph(body) {
|
|
|
508
509
|
// (plus .status/.retryAfter). Two-tier auth (Postel: liberal in what we accept): a keyless
|
|
509
510
|
// method (`keyless: true`) NEVER throws for a missing key - with a key it runs unlimited,
|
|
510
511
|
// without one the X-API-Key header is omitted entirely and the server serves the
|
|
511
|
-
// rate-limited keyless taste.
|
|
512
|
+
// rate-limited keyless taste. No method is refused here; the graph decides what to answer.
|
|
512
513
|
// The key is only ever sent in X-API-Key: never logged, never on argv, never returned.
|
|
513
|
-
async function callGraph(cypher, params, opts = {}
|
|
514
|
+
async function callGraph(cypher, params, opts = {}) {
|
|
515
|
+
// Proxy up with whatever key there is, or none: the graph owns the keyless policy and
|
|
516
|
+
// decides what to answer. The SDK adds no gate the server did not ask for.
|
|
514
517
|
const key = resolveKey(opts) || "";
|
|
515
|
-
if (!key && !keyless) {
|
|
516
|
-
throw new WhisperError(
|
|
517
|
-
`\`graph.${methodName}\` needs a Whisper API key: pass { apiKey } or set WHISPER_API_KEY. ` +
|
|
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().)"
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
518
|
const url = graphUrl();
|
|
523
519
|
const timeout = opts.timeout || 60000;
|
|
524
520
|
const ctrl = new AbortController();
|
|
@@ -619,14 +615,14 @@ function graphValue(name, value) {
|
|
|
619
615
|
}
|
|
620
616
|
|
|
621
617
|
/**
|
|
622
|
-
* Run raw Cypher against the Whisper Security graph
|
|
618
|
+
* Run raw Cypher against the Whisper Security graph. The escape hatch under every
|
|
623
619
|
* typed method: `graph.query("MATCH (n) RETURN n LIMIT 1", {}, { apiKey })`. Returns the
|
|
624
620
|
* full `{ columns, rows, statistics }` (rows are objects keyed by column name).
|
|
625
621
|
* @see https://www.whisper.security/docs/cypher-api
|
|
626
622
|
*/
|
|
627
623
|
async function graphQuery(cypher, params, opts = {}) {
|
|
628
624
|
if (!cypher || !String(cypher).trim()) throw new WhisperError("graph.query() needs a Cypher string");
|
|
629
|
-
return callGraph(String(cypher), params || {}, opts
|
|
625
|
+
return callGraph(String(cypher), params || {}, opts);
|
|
630
626
|
}
|
|
631
627
|
|
|
632
628
|
/**
|
|
@@ -650,7 +646,7 @@ function graphRecipes() {
|
|
|
650
646
|
|
|
651
647
|
// ── Flow runner (gallery/run, Server-Sent Events) ───────────────────────────────────
|
|
652
648
|
// A flow entry is a multi-step read workflow orchestrated by the Whisper workflow runner:
|
|
653
|
-
// POST {slug, value, paramValues} to the gallery/run endpoint (
|
|
649
|
+
// POST {slug, value, paramValues} to the gallery/run endpoint (X-API-Key when set) and consume
|
|
654
650
|
// the SSE stream. Each `step` event carries a step's bound Cypher + rows; the terminal
|
|
655
651
|
// `complete` event carries the aggregate context. callFlow() collects the whole stream and
|
|
656
652
|
// resolves to { slug, steps, complete, events } - one call, the full flow.
|
|
@@ -732,19 +728,15 @@ function parseSseRecord(record) {
|
|
|
732
728
|
return { event, data };
|
|
733
729
|
}
|
|
734
730
|
|
|
735
|
-
// The one place that talks to the workflow runner.
|
|
736
|
-
// POSTs {slug, value, paramValues},
|
|
731
|
+
// The one place that talks to the workflow runner. Sends the key when there is one (shared
|
|
732
|
+
// resolveKey) and goes without when there is not, POSTs {slug, value, paramValues},
|
|
733
|
+
// consumes the SSE stream, and resolves to the collected
|
|
737
734
|
// { slug, steps, complete, events }. Flows are multi-step, so the default timeout is 5
|
|
738
735
|
// minutes (override with opts.timeout). On an HTTP error, an `error` event, or a stream
|
|
739
736
|
// that ends with no result at all, throws a WhisperError with the server's clear detail.
|
|
740
737
|
async function callFlow(m, input, opts = {}) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
throw new WhisperError(
|
|
744
|
-
`\`graph.${m.name}\` needs a Whisper API key: pass { apiKey } or set WHISPER_API_KEY. ` +
|
|
745
|
-
"(The graph's direct read verbs run keyless; flows run keyed on the workflow runner.)"
|
|
746
|
-
);
|
|
747
|
-
}
|
|
738
|
+
// Flows proxy up like every other graph verb; the runner decides on the key.
|
|
739
|
+
const key = resolveKey(opts) || "";
|
|
748
740
|
const body = flowWireBody(m, input);
|
|
749
741
|
const url = flowRunUrl();
|
|
750
742
|
const timeout = opts.timeout || 300000;
|
|
@@ -753,14 +745,17 @@ async function callFlow(m, input, opts = {}) {
|
|
|
753
745
|
try {
|
|
754
746
|
let res;
|
|
755
747
|
try {
|
|
748
|
+
const headers = {
|
|
749
|
+
"Content-Type": "application/json",
|
|
750
|
+
Accept: "text/event-stream",
|
|
751
|
+
"User-Agent": UA,
|
|
752
|
+
};
|
|
753
|
+
// Present => keyed. ABSENT => keyless: an empty credential is not the same as no
|
|
754
|
+
// credential, and a server handed "" refuses instead of applying keyless policy.
|
|
755
|
+
if (key) headers["X-API-Key"] = key;
|
|
756
756
|
res = await fetch(url, {
|
|
757
757
|
method: "POST",
|
|
758
|
-
headers
|
|
759
|
-
"Content-Type": "application/json",
|
|
760
|
-
Accept: "text/event-stream",
|
|
761
|
-
"X-API-Key": key,
|
|
762
|
-
"User-Agent": UA,
|
|
763
|
-
},
|
|
758
|
+
headers,
|
|
764
759
|
body: JSON.stringify(body),
|
|
765
760
|
signal: ctrl.signal,
|
|
766
761
|
});
|
|
@@ -868,7 +863,7 @@ function makeMapCall(m) {
|
|
|
868
863
|
}
|
|
869
864
|
const keys = Object.keys(bound).sort();
|
|
870
865
|
const cypher = `CALL ${m.proc}({` + keys.map((k) => `${k}:$${k}`).join(", ") + "})";
|
|
871
|
-
return callGraph(cypher, bound, opts
|
|
866
|
+
return callGraph(cypher, bound, opts);
|
|
872
867
|
};
|
|
873
868
|
}
|
|
874
869
|
|
|
@@ -886,9 +881,9 @@ for (const m of GRAPH_METHODS) {
|
|
|
886
881
|
} else if (m.fields) {
|
|
887
882
|
graph[m.name] = makeMapCall(m);
|
|
888
883
|
} else if (m.input) {
|
|
889
|
-
graph[m.name] = async (value, opts = {}) => callGraph(m.cypher, { [m.input]: graphValue(m.name, value) }, opts
|
|
884
|
+
graph[m.name] = async (value, opts = {}) => callGraph(m.cypher, { [m.input]: graphValue(m.name, value) }, opts);
|
|
890
885
|
} else {
|
|
891
|
-
graph[m.name] = async (opts = {}) => callGraph(m.cypher, {}, opts
|
|
886
|
+
graph[m.name] = async (opts = {}) => callGraph(m.cypher, {}, opts);
|
|
892
887
|
}
|
|
893
888
|
graph[m.name].docs = m.docs;
|
|
894
889
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whisper-id",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Give any Node agent the Whisper security graph (keyless reads
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Give any Node agent the Whisper security graph (keyless reads, no client-side gate), a real routable Whisper IPv6 identity, safe egress, and the full control plane.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"index.js",
|
|
15
15
|
"index.d.ts",
|
|
16
16
|
"README.md",
|
|
17
|
+
"SECURITY.md",
|
|
17
18
|
"LICENSE"
|
|
18
19
|
],
|
|
19
20
|
"engines": {
|