whisper-id 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -3
- package/index.d.ts +414 -55
- package/index.js +253 -74
- package/package.json +33 -7
package/README.md
CHANGED
|
@@ -109,9 +109,21 @@ await graph.query("MATCH (n) RETURN n LIMIT $lim", { lim: 5 }); // raw Cypher e
|
|
|
109
109
|
| `graph.query(cypher, params)` | Raw Cypher escape hatch. → the full `{ columns, rows, statistics }` |
|
|
110
110
|
|
|
111
111
|
Fifteen multi-step **workflow** methods (`attackPath`, `attackSurface`, `blastRadius`, `typosquat`,
|
|
112
|
-
`indicator`, `routeHealth`, and 9 more)
|
|
113
|
-
|
|
114
|
-
|
|
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.
|
|
115
127
|
|
|
116
128
|
### Egress + CLI-backed
|
|
117
129
|
|
package/index.d.ts
CHANGED
|
@@ -151,13 +151,15 @@ 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 (keyed Cypher over the Whisper Security graph)
|
|
154
|
+
// ── Graph (keyed: Cypher + flows over the Whisper Security graph) ────────────────────
|
|
155
155
|
// The intelligence half of the keyed surface. Postel two-tier: the keyless identity calls
|
|
156
156
|
// above (verify/verifyDetails/rdap) run with no key and no Cypher; this whole `graph`
|
|
157
|
-
// namespace is KEYED (Kaveh's rule: if it is Cypher, it needs a key).
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
//
|
|
157
|
+
// namespace is KEYED (Kaveh's rule: if it is Cypher, it needs a key). The 14 `direct`
|
|
158
|
+
// methods POST their catalog Cypher to /api/query; the 15 `flow` methods execute on the
|
|
159
|
+
// workflow runner (POST gallery/run, Server-Sent Events) and resolve to the collected
|
|
160
|
+
// stream. The key rides only in `X-API-Key`. The method surface below is GENERATED from
|
|
161
|
+
// the Whisper query catalog by scripts/gen-graph.mjs; every method's docs live at the
|
|
162
|
+
// `@see` URL and are also on the function itself as `.docs`.
|
|
161
163
|
|
|
162
164
|
/** Per-call options for the keyed graph namespace (same shape as the control plane). */
|
|
163
165
|
export interface GraphOptions extends ControlOptions {}
|
|
@@ -172,78 +174,435 @@ export interface GraphResult {
|
|
|
172
174
|
/** A `graph.submit` payload: an indicator observation or a piece of feedback. */
|
|
173
175
|
export interface GraphSubmission {
|
|
174
176
|
kind: "indicator" | "feedback" | string;
|
|
177
|
+
identifier_kind?: string;
|
|
178
|
+
identifierKind?: string;
|
|
179
|
+
value?: string;
|
|
180
|
+
observation_id?: string;
|
|
181
|
+
observationId?: string;
|
|
182
|
+
confidence?: number;
|
|
183
|
+
first_seen?: string;
|
|
184
|
+
firstSeen?: string;
|
|
185
|
+
provenance?: string;
|
|
186
|
+
query?: string;
|
|
187
|
+
results?: unknown;
|
|
188
|
+
comment?: string;
|
|
189
|
+
severity?: string;
|
|
190
|
+
v?: number;
|
|
175
191
|
[k: string]: unknown;
|
|
176
192
|
}
|
|
177
193
|
|
|
178
194
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
195
|
+
* Input for a flow method: a bare value for the flow's primary input, a plain object of
|
|
196
|
+
* named inputs (a known knob name routes to params), or an explicit `{ inputs, params }`.
|
|
197
|
+
*/
|
|
198
|
+
export type GraphFlowInput =
|
|
199
|
+
| string
|
|
200
|
+
| number
|
|
201
|
+
| Record<string, unknown>
|
|
202
|
+
| { inputs?: Record<string, unknown>; params?: Record<string, unknown> };
|
|
203
|
+
|
|
204
|
+
/** One Server-Sent Event from a flow run; `data` is JSON-decoded when possible. */
|
|
205
|
+
export interface GraphFlowEvent {
|
|
206
|
+
event: string;
|
|
207
|
+
data: unknown;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Result of a flow run: every `step` event's payload (each carries the step's bound
|
|
212
|
+
* Cypher, columns, and rows), the terminal `complete` payload (the aggregate context;
|
|
213
|
+
* `null` if the stream ended early), and the full raw event list.
|
|
214
|
+
*/
|
|
215
|
+
export interface GraphFlowResult {
|
|
216
|
+
slug: string;
|
|
217
|
+
steps: Record<string, unknown>[];
|
|
218
|
+
complete: Record<string, unknown> | null;
|
|
219
|
+
events: GraphFlowEvent[];
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* The keyed graph namespace: one typed method per catalog entry (14 direct Cypher reads +
|
|
224
|
+
* 15 workflow-runner flows), plus a raw `query` escape hatch. Every method needs a key
|
|
225
|
+
* (`{ apiKey }` or `WHISPER_API_KEY`) and carries its canonical docs URL as `.docs`.
|
|
181
226
|
*/
|
|
182
227
|
export interface GraphNamespace {
|
|
183
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* Raw Cypher escape hatch: run arbitrary Cypher with `params` bound server-side as
|
|
230
|
+
* $-parameters. Returns the full `{columns,rows,statistics}`.
|
|
231
|
+
* @see https://www.whisper.security/docs/cypher-api
|
|
232
|
+
*/
|
|
184
233
|
query(cypher: string, params?: Record<string, unknown>, opts?: GraphOptions): Promise<GraphResult>;
|
|
185
234
|
|
|
186
|
-
|
|
235
|
+
// BEGIN GENERATED: graph catalog (scripts/gen-graph.mjs) - do not edit by hand
|
|
236
|
+
/**
|
|
237
|
+
* Assess how resilient a country's core DNS is if it were cut off from the world.
|
|
238
|
+
*
|
|
239
|
+
* Could a country still resolve names if it were isolated? Counts how many of the 13 root
|
|
240
|
+
* letters have in-country DNS_ROOT_INSTANCE anycast nodes, the Global-vs-Local split, the BGP
|
|
241
|
+
* origin ASNs hosting them, and a resilience grade.
|
|
242
|
+
*
|
|
243
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "anycast-dns-root-sovereignty")
|
|
244
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
245
|
+
* Inputs: country. Params: instanceType.
|
|
246
|
+
* @see https://www.whisper.security/docs/recipes/compliance
|
|
247
|
+
*/
|
|
248
|
+
anycastDnsRootSovereignty(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Find the choke points an attacker would target - and how any two things connect.
|
|
252
|
+
*
|
|
253
|
+
* From a starting foothold, finds shared dependencies whose compromise reaches furthest; for
|
|
254
|
+
* any two indicators, traces how they are actually connected.
|
|
255
|
+
*
|
|
256
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "attack-path")
|
|
257
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
258
|
+
* Inputs: value, other. Params: level.
|
|
259
|
+
* @see https://www.whisper.security/docs/recipes/attack-path
|
|
260
|
+
*/
|
|
261
|
+
attackPath(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Map everything about a domain that's exposed to the outside world, scored for risk.
|
|
265
|
+
*
|
|
266
|
+
* Maps the full external footprint - subdomains, name/mail servers, registrant, third-party
|
|
267
|
+
* services, connected web - and scores the exposure.
|
|
268
|
+
*
|
|
269
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "attack-surface")
|
|
270
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
271
|
+
* Inputs: domain. Params: level.
|
|
272
|
+
* @see https://www.whisper.security/docs/recipes/pentest-recon
|
|
273
|
+
*/
|
|
274
|
+
attackSurface(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Grade a network's routing security and trace conflicts to the domains they'd expose.
|
|
278
|
+
*
|
|
279
|
+
* Grades a network on the conflicts/gaps that make route hijacking possible, then traces any
|
|
280
|
+
* conflict to the specific domains and organisations exposed on the affected blocks.
|
|
281
|
+
*
|
|
282
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "bgp-hijack-exposure")
|
|
283
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
284
|
+
* Inputs: value.
|
|
285
|
+
* @see https://www.whisper.security/docs/recipes/bgp-routing
|
|
286
|
+
*/
|
|
287
|
+
bgpHijackExposure(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Pick one asset and see what would break if it failed - and what it depends on in turn.
|
|
291
|
+
*
|
|
292
|
+
* Maps dependencies in both directions: everything that breaks if the asset fails (SPOFs) and
|
|
293
|
+
* everything it relies on (its own DNS/mail/hosting/network supply chain).
|
|
294
|
+
*
|
|
295
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "blast-radius")
|
|
296
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
297
|
+
* Inputs: indicator. Params: depth.
|
|
298
|
+
* @see https://www.whisper.security/docs/recipes/soc
|
|
299
|
+
*/
|
|
300
|
+
blastRadius(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Assemble a ready-to-submit dossier for taking down a scam or phishing domain.
|
|
304
|
+
*
|
|
305
|
+
* One-pass takedown package: reputation verdict, owner (WHOIS), abuse-list listings, and
|
|
306
|
+
* surrounding infrastructure, laid out ready to hand to a registrar/host.
|
|
307
|
+
*
|
|
308
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "build-takedown-evidence-package")
|
|
309
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
310
|
+
* Inputs: domain.
|
|
311
|
+
* @see https://www.whisper.security/docs/recipes/threat-intel
|
|
312
|
+
*/
|
|
313
|
+
buildTakedownEvidencePackage(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Map an organisation's externally visible AI and agent endpoints from the outside.
|
|
317
|
+
*
|
|
318
|
+
* Maps externally visible AI/agent hosts (API, model, agent) from the outside via heuristic
|
|
319
|
+
* hostname patterns (api./mcp./ai./vector./llm./agent./chat./copilot.). Best-effort leads.
|
|
320
|
+
*
|
|
321
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "discover-ai-agent-infrastructure")
|
|
322
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
323
|
+
* Inputs: value.
|
|
324
|
+
* @see https://www.whisper.security/docs/recipes/pentest-recon
|
|
325
|
+
*/
|
|
326
|
+
discoverAiAgentInfrastructure(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Investigate one suspicious domain, IP, or network in depth and get a clear picture of the threat and everything connected to it.
|
|
330
|
+
*
|
|
331
|
+
* Deep-dive: works outward across the whole footprint (related domains, real origins behind
|
|
332
|
+
* CDN, neighbouring infra), threat-checks each node, never inherits maliciousness from shared
|
|
333
|
+
* infra; labelled posture, no score.
|
|
334
|
+
*
|
|
335
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "indicator")
|
|
336
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
337
|
+
* Inputs: indicator.
|
|
338
|
+
* @see https://www.whisper.security/docs/recipes/soc
|
|
339
|
+
*/
|
|
340
|
+
indicator(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Turn one domain or IP into a full context card - owner, hosting, mail, location, reputation at a glance.
|
|
344
|
+
*
|
|
345
|
+
* Fills the picture for one indicator: registrant (WHOIS), hosting + country, mail/name
|
|
346
|
+
* servers, network behind it, reputation read.
|
|
347
|
+
*
|
|
348
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "indicator-enrichment")
|
|
349
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
350
|
+
* Inputs: value.
|
|
351
|
+
* @see https://www.whisper.security/docs/recipes/dns-email
|
|
352
|
+
*/
|
|
353
|
+
indicatorEnrichment(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Trace one indicator to its true owner and full estate, even behind privacy screens and CDNs.
|
|
357
|
+
*
|
|
358
|
+
* Works out the true operator (even behind privacy WHOIS), de-cloaks CDN-fronted sites to real
|
|
359
|
+
* servers, pivots to the rest of that owner's estate across every layer.
|
|
360
|
+
*
|
|
361
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "infrastructure-mapping")
|
|
362
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
363
|
+
* Inputs: value. Params: level.
|
|
364
|
+
* @see https://www.whisper.security/docs/recipes/compliance
|
|
365
|
+
*/
|
|
366
|
+
infrastructureMapping(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Grade an organisation for over-reliance on single providers, regions, or facilities.
|
|
370
|
+
*
|
|
371
|
+
* Grades how concentrated infra is - too much riding on one provider/region/data-centre/cable
|
|
372
|
+
* landing - surfacing SPOFs for resilience and DORA/NIS2 fourth-party risk.
|
|
373
|
+
*
|
|
374
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "map-supply-chain-concentration")
|
|
375
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
376
|
+
* Inputs: domain.
|
|
377
|
+
* @see https://www.whisper.security/docs/recipes/compliance
|
|
378
|
+
*/
|
|
379
|
+
mapSupplyChainConcentration(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Check a domain's name servers for the misconfigurations that enable DNS hijacking.
|
|
383
|
+
*
|
|
384
|
+
* Audits delegation, flags stale/mismatched/lame nameservers, sizes each provider's share,
|
|
385
|
+
* surfaces registry facts - catches delegation weakness before exploit.
|
|
386
|
+
*
|
|
387
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "nameserver-hijack-dns-consistency")
|
|
388
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
389
|
+
* Inputs: value.
|
|
390
|
+
* @see https://www.whisper.security/docs/recipes/dns-email
|
|
391
|
+
*/
|
|
392
|
+
nameserverHijackDnsConsistency(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Profile a network or address block into a full routing and reachability health card.
|
|
396
|
+
*
|
|
397
|
+
* Health card for a network/block: what it announces, peers/transit, single-upstream lean,
|
|
398
|
+
* RPKI protection - the one-look reach & resilience report.
|
|
399
|
+
*
|
|
400
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "route-health")
|
|
401
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
402
|
+
* Inputs: target.
|
|
403
|
+
* @see https://www.whisper.security/docs/recipes/bgp-routing
|
|
404
|
+
*/
|
|
405
|
+
routeHealth(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Find subdomains that point at abandoned services an attacker could claim.
|
|
409
|
+
*
|
|
410
|
+
* Walks subdomains and flags ones aiming at deprovisioned targets (dangling CNAME whose target
|
|
411
|
+
* no longer resolves) so you can reclaim/remove before someone else does. CNAME layer is
|
|
412
|
+
* prod-ahead.
|
|
413
|
+
*
|
|
414
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "subdomain-takeover")
|
|
415
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
416
|
+
* Inputs: value.
|
|
417
|
+
* @see https://www.whisper.security/docs/recipes/pentest-recon
|
|
418
|
+
*/
|
|
419
|
+
subdomainTakeover(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Find registered look-alikes of your brand and check which ones are dangerous.
|
|
423
|
+
*
|
|
424
|
+
* Finds registered impersonations across misspellings/risky TLDs, separates your own defensive
|
|
425
|
+
* domains from strangers', flags fresh/privacy/abuse-listed ones into a prioritised list.
|
|
426
|
+
*
|
|
427
|
+
* KEYED flow: streams Server-Sent Events from the workflow runner (slug "typosquat")
|
|
428
|
+
* and resolves to the collected { slug, steps, complete, events }.
|
|
429
|
+
* Inputs: domain.
|
|
430
|
+
* @see https://www.whisper.security/docs/recipes/brand-protection
|
|
431
|
+
*/
|
|
432
|
+
typosquat(input?: GraphFlowInput, opts?: GraphOptions): Promise<GraphFlowResult>;
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Name the vendor and operator role behind a host or IP in one call.
|
|
436
|
+
*
|
|
437
|
+
* Resolves a host/IP to its canonical vendor, category and operator roles (DNS_OPERATOR / CDN
|
|
438
|
+
* / ORIGIN_AS / MAIL_RECEIVER) with a confidence band.
|
|
439
|
+
*
|
|
440
|
+
* KEYED. Cypher: CALL whisper.identify([$v]) YIELD host, vendor_id, canonical_name, category, roles, host_class, band
|
|
441
|
+
* Returns columns: host, vendor_id, canonical_name, category, roles, host_class, band.
|
|
442
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures/identify
|
|
443
|
+
*/
|
|
187
444
|
identify(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
188
|
-
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Get a labelled threat posture for a host or IP - malicious, benign, or unknown.
|
|
448
|
+
*
|
|
449
|
+
* Returns a posture label + severity band + sub-labels + coverage + evidence for an indicator;
|
|
450
|
+
* benign-allowlisted vs malicious-evidenced, never a bare score.
|
|
451
|
+
*
|
|
452
|
+
* KEYED. Cypher: CALL whisper.assess([$v]) YIELD host, label, band, sub_labels, coverage, evidence
|
|
453
|
+
* Returns columns: host, label, band, sub_labels, coverage, evidence.
|
|
454
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures
|
|
455
|
+
*/
|
|
189
456
|
assess(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
190
|
-
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Generate look-alike domain variants of a brand and see which are registered.
|
|
460
|
+
*
|
|
461
|
+
* Enumerates permutations (omission, bitsquatting, ...) of a domain, flags which exist, with a
|
|
462
|
+
* per-variant confidence.
|
|
463
|
+
*
|
|
464
|
+
* KEYED. Cypher: CALL whisper.variants($v) YIELD variant, method, exists, confidence
|
|
465
|
+
* Returns columns: variant, method, exists, confidence.
|
|
466
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures/variants
|
|
467
|
+
*/
|
|
191
468
|
variants(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
192
|
-
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Walk the graph to the nearest known vendors behind a host, with the channel and confidence.
|
|
472
|
+
*
|
|
473
|
+
* Structural attribution: returns nearest_known_vendors with the channel (DELEGATED_TO /
|
|
474
|
+
* ORIGIN_AS) and confidence, plus siblings and coverage - explains WHY a host maps to a
|
|
475
|
+
* vendor.
|
|
476
|
+
*
|
|
477
|
+
* KEYED. Cypher: CALL whisper.walk($v) YIELD coverage, host, nearest_known_vendors, no_atlas_match, siblings
|
|
478
|
+
* Returns columns: coverage, host, nearest_known_vendors, no_atlas_match, siblings.
|
|
479
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures
|
|
480
|
+
*/
|
|
193
481
|
walk(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
194
|
-
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Score an indicator against the threat feeds and explain exactly why.
|
|
485
|
+
*
|
|
486
|
+
* Threat-intel scorer: score + level + human explanation + the feeds it is listed in (feedId,
|
|
487
|
+
* weight, firstSeen/lastSeen). Two live forms: bare CALL explain($v) (full columns) and CALL
|
|
488
|
+
* whisper.explain($v) (restricted YIELD).
|
|
489
|
+
*
|
|
490
|
+
* KEYED. Cypher: CALL whisper.explain($v) YIELD indicator, score, level, explanation, sources
|
|
491
|
+
* Returns columns: indicator, score, level, explanation, sources.
|
|
492
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures/explain
|
|
493
|
+
*/
|
|
195
494
|
explain(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
196
|
-
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Reduce any hostname to its registrable apex (eTLD+1) via the Public Suffix List.
|
|
498
|
+
*
|
|
499
|
+
* PSL-correct eTLD+1: www.foo.co.uk -> foo.co.uk. The right way to group hosts by the thing
|
|
500
|
+
* someone actually registered.
|
|
501
|
+
*
|
|
502
|
+
* KEYED. Cypher: CALL whisper.psl.tldPlusOne($v) YIELD apex
|
|
503
|
+
* Returns columns: apex.
|
|
504
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures/helpers
|
|
505
|
+
*/
|
|
197
506
|
pslTldplusone(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
198
|
-
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Check whether a domain is a PSL private-section suffix and who submitted it.
|
|
510
|
+
*
|
|
511
|
+
* For a PSL private-section suffix, returns the submitting org/login and evidence kind +
|
|
512
|
+
* confidence; found=false for ordinary registrable domains.
|
|
513
|
+
*
|
|
514
|
+
* KEYED. Cypher: CALL whisper.psl.affiliation($v) YIELD found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence
|
|
515
|
+
* Returns columns: found, suffix, submitterOrg, submitterLogin, evidenceKind, confidence.
|
|
516
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures/helpers
|
|
517
|
+
*/
|
|
199
518
|
pslAffiliation(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
200
|
-
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Find the real origin IPs behind a CDN-fronted domain, ranked by confidence.
|
|
522
|
+
*
|
|
523
|
+
* Candidate origin IPs behind a CDN with confidence, the methods that found them (e.g.
|
|
524
|
+
* links_to), and the hosting ASN/name - de-cloaks to the real server.
|
|
525
|
+
*
|
|
526
|
+
* KEYED. Cypher: CALL whisper.origins($v) YIELD ip, confidence, methods, asn, asnName, kind
|
|
527
|
+
* Returns columns: ip, confidence, methods, asn, asnName, kind.
|
|
528
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures/origins
|
|
529
|
+
*/
|
|
201
530
|
origins(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
202
|
-
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Get the full historical WHOIS timeline for a domain.
|
|
534
|
+
*
|
|
535
|
+
* Every observed WHOIS snapshot over time: create/update/expiry, registrar, registrant,
|
|
536
|
+
* country, nameservers - the ownership/registration story.
|
|
537
|
+
*
|
|
538
|
+
* KEYED. Cypher: CALL whisper.history($v)
|
|
539
|
+
* Returns columns: indicator, type, queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers, cached.
|
|
540
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures/history
|
|
541
|
+
*/
|
|
203
542
|
history(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
204
|
-
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Get the WHOIS-only historical timeline for a domain.
|
|
546
|
+
*
|
|
547
|
+
* The registration-record projection of the history timeline: create/update/expiry, registrar,
|
|
548
|
+
* registrant, country, nameservers per snapshot.
|
|
549
|
+
*
|
|
550
|
+
* KEYED. Cypher: CALL whisper.history.whois($v) YIELD queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers
|
|
551
|
+
* Returns columns: queryTime, createDate, updateDate, expiryDate, registrar, registrant, country, nameServers.
|
|
552
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures/history
|
|
553
|
+
*/
|
|
205
554
|
historyWhois(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
206
|
-
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* List the member ASNs of an AS-SET macro.
|
|
558
|
+
*
|
|
559
|
+
* Expands an AS-SET name (e.g. AS-CLOUDFLARE) to its member ASNs and source RIR. Arg is a
|
|
560
|
+
* STRING as-set name, not an integer ASN. (Membership is sparsely populated in the current
|
|
561
|
+
* graph.)
|
|
562
|
+
*
|
|
563
|
+
* KEYED. Cypher: CALL whisper.asSet($v) YIELD asSetName, memberAsn, sourceRir
|
|
564
|
+
* Returns columns: asSetName, memberAsn, sourceRir.
|
|
565
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures
|
|
566
|
+
*/
|
|
207
567
|
asset(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
208
|
-
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Check whether an IP is a known Tor exit relay.
|
|
571
|
+
*
|
|
572
|
+
* Live Tor exit-node check: found + fingerprint + exit-address count + source + ingest time
|
|
573
|
+
* for an IPv4.
|
|
574
|
+
*
|
|
575
|
+
* KEYED. Cypher: CALL whisper.lookupTorRelay($v) YIELD indicator, found, fingerprint, exitAddressCount, source, ingestedAt
|
|
576
|
+
* Returns columns: indicator, found, fingerprint, exitAddressCount, source, ingestedAt.
|
|
577
|
+
* @see https://www.whisper.security/docs/whisper-graph/procedures/helpers
|
|
578
|
+
*/
|
|
209
579
|
lookupTorRelay(value: string, opts?: GraphOptions): Promise<GraphResult>;
|
|
210
|
-
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* List every node and relationship type in the graph with counts and examples.
|
|
583
|
+
*
|
|
584
|
+
* The self-describing schema: node/relationship types with counts, descriptions, examples,
|
|
585
|
+
* source/target labels and query best-practices - the map of what the graph holds.
|
|
586
|
+
*
|
|
587
|
+
* KEYED. Cypher: CALL db.schema()
|
|
588
|
+
* Returns columns: type, name, count, description, example, sourceLabels, targetLabels, fastPatterns, slowPatterns, bestPractices.
|
|
589
|
+
* @see https://www.whisper.security/docs/whisper-graph/schema
|
|
590
|
+
*/
|
|
211
591
|
dbSchema(opts?: GraphOptions): Promise<GraphResult>;
|
|
212
|
-
/** Contribute an indicator observation or feedback back into the graph (keyed WRITE). */
|
|
213
|
-
submit(payload: GraphSubmission, opts?: GraphOptions): Promise<GraphResult>;
|
|
214
592
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
buildTakedownEvidencePackage(opts?: GraphOptions): Promise<never>;
|
|
229
|
-
/** Workflow (run_workflow): map an org's externally visible AI/agent endpoints. Not wired here. */
|
|
230
|
-
discoverAiAgentInfrastructure(opts?: GraphOptions): Promise<never>;
|
|
231
|
-
/** Workflow (run_workflow): investigate one indicator in depth. Not wired here. */
|
|
232
|
-
indicator(opts?: GraphOptions): Promise<never>;
|
|
233
|
-
/** Workflow (run_workflow): turn one domain/IP into a full context card. Not wired here. */
|
|
234
|
-
indicatorEnrichment(opts?: GraphOptions): Promise<never>;
|
|
235
|
-
/** Workflow (run_workflow): trace an indicator to its true owner and estate. Not wired here. */
|
|
236
|
-
infrastructureMapping(opts?: GraphOptions): Promise<never>;
|
|
237
|
-
/** Workflow (run_workflow): grade an org for supply-chain concentration. Not wired here. */
|
|
238
|
-
mapSupplyChainConcentration(opts?: GraphOptions): Promise<never>;
|
|
239
|
-
/** Workflow (run_workflow): check name servers for DNS-hijack misconfigurations. Not wired here. */
|
|
240
|
-
nameserverHijackDnsConsistency(opts?: GraphOptions): Promise<never>;
|
|
241
|
-
/** Workflow (run_workflow): profile a network/block into a routing health card. Not wired here. */
|
|
242
|
-
routeHealth(opts?: GraphOptions): Promise<never>;
|
|
243
|
-
/** Workflow (run_workflow): find subdomains that point at claimable services. Not wired here. */
|
|
244
|
-
subdomainTakeover(opts?: GraphOptions): Promise<never>;
|
|
245
|
-
/** Workflow (run_workflow): find registered look-alikes of your brand. Not wired here. */
|
|
246
|
-
typosquat(opts?: GraphOptions): Promise<never>;
|
|
593
|
+
/**
|
|
594
|
+
* Contribute an indicator observation or feedback back into the graph (requires an API key).
|
|
595
|
+
*
|
|
596
|
+
* The write channel: submit an indicator/feedback with an attributable API key (anonymous
|
|
597
|
+
* submits are refused to preserve K-anonymity). Keyed-only by design.
|
|
598
|
+
*
|
|
599
|
+
* KEYED write: CALL whisper.submit({...}) with the payload fields bound as $-parameters.
|
|
600
|
+
* Fields: kind, identifier_kind, value, observation_id, confidence, first_seen, provenance, query, results, comment, severity, v (kind required).
|
|
601
|
+
* Returns columns: observation_id, kind, accepted, idempotent, promotion_state, k_bucket, advisory, v.
|
|
602
|
+
* @see https://www.whisper.security/docs/cypher-api
|
|
603
|
+
*/
|
|
604
|
+
submit(payload: GraphSubmission, opts?: GraphOptions): Promise<GraphResult>;
|
|
605
|
+
// END GENERATED: graph catalog
|
|
247
606
|
}
|
|
248
607
|
|
|
249
608
|
/** The keyed graph namespace. `import { graph } from "whisper-id"`. */
|
package/index.js
CHANGED
|
@@ -466,11 +466,12 @@ export async function agent(sel = {}, maybeOpts) {
|
|
|
466
466
|
// where rows are already column-keyed objects), so it has its own choke point, callGraph().
|
|
467
467
|
//
|
|
468
468
|
// Source of truth for the 29 methods is the Whisper query catalog (catalog.json +
|
|
469
|
-
// sdk-methods.json)
|
|
470
|
-
//
|
|
471
|
-
// Cypher against /api/query; 15 `flow` entries
|
|
472
|
-
//
|
|
473
|
-
//
|
|
469
|
+
// sdk-methods.json): the GRAPH_METHODS table below is GENERATED from it by
|
|
470
|
+
// scripts/gen-graph.mjs (`npm run gen`), so this SDK cannot drift from the catalog. The
|
|
471
|
+
// 14 `direct` entries run their catalog Cypher against /api/query; the 15 `flow` entries
|
|
472
|
+
// EXECUTE on the workflow runner (POST gallery/run, Server-Sent Events) via callFlow().
|
|
473
|
+
// Every method carries its canonical docs URL as `.docs`. graph.query() is the raw
|
|
474
|
+
// escape hatch.
|
|
474
475
|
|
|
475
476
|
function graphUrl() {
|
|
476
477
|
// Defaults to the control endpoint, which already IS the graph endpoint. Override for tests
|
|
@@ -550,44 +551,45 @@ async function callGraph(cypher, params, opts = {}, methodName = "query") {
|
|
|
550
551
|
return normalizeGraph(body);
|
|
551
552
|
}
|
|
552
553
|
|
|
553
|
-
//
|
|
554
|
-
//
|
|
555
|
-
// (
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
554
|
+
// BEGIN GENERATED: graph catalog (scripts/gen-graph.mjs) - do not edit by hand
|
|
555
|
+
// The Whisper query catalog, one row per entry (14 direct + 15 flow), in catalog order.
|
|
556
|
+
// A direct row carries its catalog Cypher (input bound as a $-parameter); a flow row
|
|
557
|
+
// carries its gallery/run slug plus the input/param names the workflow runner accepts;
|
|
558
|
+
// the map-call row (submit) enumerates its fields. `docs` is the canonical docs URL.
|
|
559
|
+
const GRAPH_DOCS_BASE = "https://www.whisper.security";
|
|
560
|
+
const GRAPH_FLOW_RUN_URL = "https://console.whisper.security/api/gallery/run";
|
|
561
|
+
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" },
|
|
591
|
+
];
|
|
592
|
+
// END GENERATED: graph catalog
|
|
591
593
|
|
|
592
594
|
// Coerce a liberal first arg into the string value bound as $v. Accepts a bare value or a
|
|
593
595
|
// { value } / { v } wrapper; empty is rejected with a clear error, never an opaque failure.
|
|
@@ -607,48 +609,225 @@ function graphValue(name, value) {
|
|
|
607
609
|
* Run raw Cypher against the Whisper Security graph (keyed). The escape hatch under every
|
|
608
610
|
* typed method: `graph.query("MATCH (n) RETURN n LIMIT 1", {}, { apiKey })`. Returns the
|
|
609
611
|
* full `{ columns, rows, statistics }` (rows are objects keyed by column name).
|
|
612
|
+
* @see https://www.whisper.security/docs/cypher-api
|
|
610
613
|
*/
|
|
611
614
|
async function graphQuery(cypher, params, opts = {}) {
|
|
612
615
|
if (!cypher || !String(cypher).trim()) throw new WhisperError("graph.query() needs a Cypher string");
|
|
613
616
|
return callGraph(String(cypher), params || {}, opts, "query");
|
|
614
617
|
}
|
|
615
618
|
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
619
|
+
// ── Flow runner (gallery/run, Server-Sent Events) ───────────────────────────────────
|
|
620
|
+
// A flow entry is a multi-step read workflow orchestrated by the Whisper workflow runner:
|
|
621
|
+
// POST {slug, inputs, params} to the gallery/run endpoint (keyed, X-API-Key) and consume
|
|
622
|
+
// the SSE stream. Each `step` event carries a step's bound Cypher + rows; the terminal
|
|
623
|
+
// `complete` event carries the aggregate context. callFlow() collects the whole stream and
|
|
624
|
+
// resolves to { slug, steps, complete, events } - one call, the full flow.
|
|
625
|
+
|
|
626
|
+
function flowRunUrl() {
|
|
627
|
+
return process.env.WHISPER_FLOW_URL || GRAPH_FLOW_RUN_URL;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Liberal-accept (RFC 761): coerce the caller's input into the runner's {inputs, params}.
|
|
631
|
+
// A bare value binds to the flow's primary input; a plain object routes each key by name
|
|
632
|
+
// (a known knob name goes to params, everything else to inputs); an explicit
|
|
633
|
+
// { inputs, params } wrapper passes through untouched.
|
|
634
|
+
function flowBody(m, input) {
|
|
635
|
+
const inputs = {};
|
|
636
|
+
const params = {};
|
|
637
|
+
if (input === null || input === undefined) return { inputs, params };
|
|
638
|
+
if (typeof input !== "object" || Array.isArray(input)) {
|
|
639
|
+
const v = String(input).trim();
|
|
640
|
+
if (v === "") throw new WhisperError(`graph.${m.name}() needs a value for its \`${m.primary}\` input (a host, IP, domain, ASN, or prefix)`);
|
|
641
|
+
inputs[m.primary] = v;
|
|
642
|
+
return { inputs, params };
|
|
643
|
+
}
|
|
644
|
+
if ((input.inputs && typeof input.inputs === "object") || (input.params && typeof input.params === "object")) {
|
|
645
|
+
return { inputs: { ...(input.inputs || {}) }, params: { ...(input.params || {}) } };
|
|
646
|
+
}
|
|
647
|
+
for (const [k, v] of Object.entries(input)) {
|
|
648
|
+
if (v === undefined) continue;
|
|
649
|
+
if (m.params.includes(k)) params[k] = v;
|
|
650
|
+
else inputs[k] = v;
|
|
651
|
+
}
|
|
652
|
+
return { inputs, params };
|
|
623
653
|
}
|
|
624
654
|
|
|
625
|
-
|
|
626
|
-
|
|
655
|
+
// Parse one SSE record ("event: x\ndata: {...}") into { event, data }; data is JSON-decoded
|
|
656
|
+
// when possible, else kept as the raw string. Never throws on shape.
|
|
657
|
+
function parseSseRecord(record) {
|
|
658
|
+
let event = "message";
|
|
659
|
+
const dataLines = [];
|
|
660
|
+
for (const line of record.split("\n")) {
|
|
661
|
+
if (line.startsWith("event:")) event = line.slice(6).trim();
|
|
662
|
+
else if (line.startsWith("data:")) dataLines.push(line.slice(5).replace(/^ /, ""));
|
|
663
|
+
}
|
|
664
|
+
let data = dataLines.join("\n");
|
|
665
|
+
try {
|
|
666
|
+
data = JSON.parse(data);
|
|
667
|
+
} catch {
|
|
668
|
+
// not JSON - keep the raw string
|
|
669
|
+
}
|
|
670
|
+
return { event, data };
|
|
671
|
+
}
|
|
627
672
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
return callGraph("CALL whisper.submit($submission)", { submission: p }, opts, "submit");
|
|
640
|
-
};
|
|
641
|
-
|
|
642
|
-
for (const [name, runVia] of Object.entries(GRAPH_FLOW)) {
|
|
643
|
-
graph[name] = () => {
|
|
644
|
-
const err = new WhisperError(
|
|
645
|
-
`graph.${name}() is a multi-step read workflow: it runs via the Whisper workflow runner ` +
|
|
646
|
-
`(${runVia}), not a single /api/query Cypher, and is not wired into this SDK yet. Use ` +
|
|
647
|
-
"graph.query() for a raw Cypher call, or run the workflow via the console/agent runner."
|
|
673
|
+
// The one place that talks to the workflow runner. Requires a key (shared resolveKey),
|
|
674
|
+
// POSTs {slug, inputs, params}, consumes the SSE stream, and resolves to the collected
|
|
675
|
+
// { slug, steps, complete, events }. Flows are multi-step, so the default timeout is 5
|
|
676
|
+
// minutes (override with opts.timeout). On an HTTP error, an `error` event, or a stream
|
|
677
|
+
// that ends with no result at all, throws a WhisperError with the server's clear detail.
|
|
678
|
+
async function callFlow(m, input, opts = {}) {
|
|
679
|
+
const key = resolveKey(opts);
|
|
680
|
+
if (!key) {
|
|
681
|
+
throw new WhisperError(
|
|
682
|
+
`\`graph.${m.name}\` needs a Whisper API key: pass { apiKey } or set WHISPER_API_KEY. ` +
|
|
683
|
+
"(verify/verifyDetails/rdap are keyless and need no key; flows run keyed on the workflow runner.)"
|
|
648
684
|
);
|
|
649
|
-
|
|
650
|
-
|
|
685
|
+
}
|
|
686
|
+
const { inputs, params } = flowBody(m, input);
|
|
687
|
+
const url = flowRunUrl();
|
|
688
|
+
const timeout = opts.timeout || 300000;
|
|
689
|
+
const ctrl = new AbortController();
|
|
690
|
+
const t = setTimeout(() => ctrl.abort(), timeout);
|
|
691
|
+
try {
|
|
692
|
+
let res;
|
|
693
|
+
try {
|
|
694
|
+
res = await fetch(url, {
|
|
695
|
+
method: "POST",
|
|
696
|
+
headers: {
|
|
697
|
+
"Content-Type": "application/json",
|
|
698
|
+
Accept: "text/event-stream",
|
|
699
|
+
"X-API-Key": key,
|
|
700
|
+
"User-Agent": UA,
|
|
701
|
+
},
|
|
702
|
+
body: JSON.stringify({ slug: m.slug, inputs, params }),
|
|
703
|
+
signal: ctrl.signal,
|
|
704
|
+
});
|
|
705
|
+
} catch (err) {
|
|
706
|
+
throw new WhisperError(`could not reach the Whisper workflow runner at ${url}: ${err && err.message ? err.message : err}`);
|
|
707
|
+
}
|
|
708
|
+
if (res.status >= 400) {
|
|
709
|
+
let body = null;
|
|
710
|
+
try {
|
|
711
|
+
body = await res.json();
|
|
712
|
+
} catch {
|
|
713
|
+
body = null;
|
|
714
|
+
}
|
|
715
|
+
const e = body && typeof body === "object" ? body : {};
|
|
716
|
+
const werr = new WhisperError(
|
|
717
|
+
e.message || e.detail || (typeof e.error === "string" ? e.error : null) || `the workflow runner returned HTTP ${res.status}`
|
|
718
|
+
);
|
|
719
|
+
werr.status = res.status;
|
|
720
|
+
throw werr;
|
|
721
|
+
}
|
|
722
|
+
// Liberal accept: a plain-JSON reply (no stream) is decoded as the complete result.
|
|
723
|
+
const ct = res.headers && typeof res.headers.get === "function" ? res.headers.get("content-type") || "" : "";
|
|
724
|
+
if (ct.includes("json")) {
|
|
725
|
+
let body = null;
|
|
726
|
+
try {
|
|
727
|
+
body = await res.json();
|
|
728
|
+
} catch {
|
|
729
|
+
body = null;
|
|
730
|
+
}
|
|
731
|
+
return { slug: m.slug, steps: [], complete: body && typeof body === "object" ? body : null, events: [] };
|
|
732
|
+
}
|
|
733
|
+
// The SSE stream: split records on blank lines (CRLF-safe), decode each.
|
|
734
|
+
const events = [];
|
|
735
|
+
const decoder = new TextDecoder("utf-8");
|
|
736
|
+
let buf = "";
|
|
737
|
+
const drain = () => {
|
|
738
|
+
buf = buf.replace(/\r\n/g, "\n");
|
|
739
|
+
let i;
|
|
740
|
+
while ((i = buf.indexOf("\n\n")) !== -1) {
|
|
741
|
+
const rec = buf.slice(0, i);
|
|
742
|
+
buf = buf.slice(i + 2);
|
|
743
|
+
if (rec.trim() !== "") events.push(parseSseRecord(rec));
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
for await (const chunk of res.body) {
|
|
747
|
+
buf += decoder.decode(chunk, { stream: true });
|
|
748
|
+
drain();
|
|
749
|
+
}
|
|
750
|
+
buf += decoder.decode();
|
|
751
|
+
drain();
|
|
752
|
+
if (buf.trim() !== "") events.push(parseSseRecord(buf));
|
|
753
|
+
const steps = events.filter((ev) => ev.event === "step").map((ev) => ev.data);
|
|
754
|
+
const completeEv = events.findLast((ev) => ev.event === "complete");
|
|
755
|
+
const complete = completeEv && completeEv.data && typeof completeEv.data === "object" ? completeEv.data : null;
|
|
756
|
+
const errEv = events.find((ev) => ev.event === "error");
|
|
757
|
+
if (errEv && !complete) {
|
|
758
|
+
const d = errEv.data && typeof errEv.data === "object" ? errEv.data : { detail: String(errEv.data || "") };
|
|
759
|
+
const werr = new WhisperError(d.message || d.detail || (typeof d.error === "string" ? d.error : null) || `graph.${m.name} failed on the workflow runner`);
|
|
760
|
+
werr.events = events;
|
|
761
|
+
throw werr;
|
|
762
|
+
}
|
|
763
|
+
if (!complete && steps.length === 0) {
|
|
764
|
+
const werr = new WhisperError(`graph.${m.name}: the flow stream ended without a step or complete event`);
|
|
765
|
+
werr.events = events;
|
|
766
|
+
throw werr;
|
|
767
|
+
}
|
|
768
|
+
return { slug: m.slug, steps, complete, events };
|
|
769
|
+
} catch (err) {
|
|
770
|
+
if (err instanceof WhisperError) throw err;
|
|
771
|
+
if ((err && err.name === "AbortError") || ctrl.signal.aborted) {
|
|
772
|
+
throw new WhisperError(`graph.${m.name} timed out after ${timeout}ms (flows are multi-step; raise { timeout } if you need longer)`);
|
|
773
|
+
}
|
|
774
|
+
throw new WhisperError(`graph.${m.name} failed while streaming from the workflow runner: ${err && err.message ? err.message : err}`);
|
|
775
|
+
} finally {
|
|
776
|
+
clearTimeout(t);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// The map-call write (whisper.submit): each payload field becomes one entry of a Cypher
|
|
781
|
+
// map with its value bound as its own $-parameter (injection-safe), keys in SORTED order
|
|
782
|
+
// so the query is byte-stable. Field names are accepted in both catalog spellings
|
|
783
|
+
// (camelCase id or snake_case paramName) and normalised to the wire form; any extra key
|
|
784
|
+
// must be a plain identifier, and the server validates the full set per kind.
|
|
785
|
+
function makeMapCall(m) {
|
|
786
|
+
const canon = new Map();
|
|
787
|
+
for (const [id, param] of m.fields) {
|
|
788
|
+
canon.set(id, param);
|
|
789
|
+
canon.set(param, param);
|
|
790
|
+
}
|
|
791
|
+
return async (payload, opts = {}) => {
|
|
792
|
+
const p = payload && typeof payload === "object" && !Array.isArray(payload) ? payload : {};
|
|
793
|
+
const bound = {};
|
|
794
|
+
for (const [k, v] of Object.entries(p)) {
|
|
795
|
+
if (v === undefined) continue;
|
|
796
|
+
const name = canon.get(k) || k;
|
|
797
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
798
|
+
throw new WhisperError(`graph.${m.name}() field ${JSON.stringify(k)} is not a valid field name (letters, digits, _)`);
|
|
799
|
+
}
|
|
800
|
+
bound[name] = v;
|
|
801
|
+
}
|
|
802
|
+
for (const req of m.requires) {
|
|
803
|
+
if (bound[req] === null || bound[req] === undefined || String(bound[req]).trim() === "") {
|
|
804
|
+
throw new WhisperError(`graph.${m.name}() needs a { ${req} } of 'indicator' or 'feedback' (plus its fields)`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
const keys = Object.keys(bound).sort();
|
|
808
|
+
const cypher = `CALL ${m.proc}({` + keys.map((k) => `${k}:$${k}`).join(", ") + "})";
|
|
809
|
+
return callGraph(cypher, bound, opts, m.name);
|
|
651
810
|
};
|
|
652
811
|
}
|
|
653
812
|
|
|
813
|
+
// Build the namespace from the generated catalog table: query + one method per entry.
|
|
814
|
+
// Direct methods run their catalog Cypher; the map-call write builds its parameter map;
|
|
815
|
+
// flow methods execute on the workflow runner. Every method carries its canonical docs
|
|
816
|
+
// URL as `.docs` (docsBase + the entry's docPath).
|
|
817
|
+
const graph = { query: graphQuery };
|
|
818
|
+
graph.query.docs = GRAPH_DOCS_BASE + "/docs/cypher-api";
|
|
819
|
+
|
|
820
|
+
for (const m of GRAPH_METHODS) {
|
|
821
|
+
if (m.mode === "flow") {
|
|
822
|
+
graph[m.name] = async (input, opts = {}) => callFlow(m, input, opts);
|
|
823
|
+
} else if (m.fields) {
|
|
824
|
+
graph[m.name] = makeMapCall(m);
|
|
825
|
+
} else if (m.input) {
|
|
826
|
+
graph[m.name] = async (value, opts = {}) => callGraph(m.cypher, { [m.input]: graphValue(m.name, value) }, opts, m.name);
|
|
827
|
+
} else {
|
|
828
|
+
graph[m.name] = async (opts = {}) => callGraph(m.cypher, {}, opts, m.name);
|
|
829
|
+
}
|
|
830
|
+
graph[m.name].docs = m.docs;
|
|
831
|
+
}
|
|
832
|
+
|
|
654
833
|
export { graph };
|
package/package.json
CHANGED
|
@@ -1,16 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "whisper-id",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A routable Whisper IPv6 identity and safe egress for any Node agent.",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"exports": {
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"import": "./index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
7
12
|
"types": "./index.d.ts",
|
|
8
|
-
"files": [
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
13
|
+
"files": [
|
|
14
|
+
"index.js",
|
|
15
|
+
"index.d.ts",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "node --test",
|
|
24
|
+
"gen": "node scripts/gen-graph.mjs"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"whisper",
|
|
28
|
+
"agent",
|
|
29
|
+
"identity",
|
|
30
|
+
"ipv6",
|
|
31
|
+
"egress",
|
|
32
|
+
"proxy",
|
|
33
|
+
"dns"
|
|
34
|
+
],
|
|
12
35
|
"homepage": "https://whisper.online",
|
|
13
|
-
"repository": {
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/whisper-sec/whisper-node.git"
|
|
39
|
+
},
|
|
14
40
|
"license": "MIT",
|
|
15
41
|
"author": "Whisper Security"
|
|
16
42
|
}
|