echelongraph-mcp 1.0.2 → 1.0.3
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 +30 -8
- package/dist/index.js +178 -29
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -54,11 +54,31 @@ The radar only looks for its **tracked** set of CVEs, so a zero means different
|
|
|
54
54
|
|
|
55
55
|
### How `exposure_radar` counts shadow AI
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
The shadow-AI radar's answer mixes numbers that count exposed services with numbers that count
|
|
58
|
+
every observation. So `exposure_radar` regroups it by what each number counts, and its note
|
|
59
|
+
labels each one. Only `shadow_ai.confirmed_exposed` counts exposed services.
|
|
60
|
+
|
|
61
|
+
| Field | What it counts |
|
|
62
|
+
|---|---|
|
|
63
|
+
| `confirmed_exposed.total` | Confirmed exposed: services EchelonGraph's probes found answering without an authentication gate (liveness `active`, or `rechecking` during a re-check). It is the sum of `confirmed_exposed.by_category`. |
|
|
64
|
+
| `confirmed_exposed.by_category` | The same services, by category. |
|
|
65
|
+
| `confirmed_exposed.last_24h` | Confirmed-exposed services first recorded in the last 24 hours. |
|
|
66
|
+
| `observed.total` | Every Certificate Transparency or Shodan observation on record, whatever its verification state: observed, not exposed. |
|
|
67
|
+
| `observed.by_category` | The same observations, by category. |
|
|
68
|
+
| `observed.last_24h` | Observations first recorded in the last 24 hours. |
|
|
69
|
+
| `observed.trend_30d` | Observations per UTC day over the last 30 days. |
|
|
70
|
+
| `observed.top_products`, `observed.top_countries`, `observed.top_issuers` | Up to ten products, countries and issuers, ranked by observations, not by exposed services. An issuer is the certificate's CA for a Certificate Transparency observation, and the hosting operator Shodan reports for a Shodan one. |
|
|
71
|
+
| `observed.last_observation` | When the latest observation was recorded. |
|
|
72
|
+
| `authentication.observed` | Observations where a probe observed an authentication gate: a 401/403, a login page or an auth marker. |
|
|
73
|
+
| `authentication.not_determined` | Observations whose service answered, but where no probe could tell whether it enforces authentication. |
|
|
74
|
+
|
|
75
|
+
Neither authentication count is part of `confirmed_exposed`, and `observed.total` minus
|
|
76
|
+
`confirmed_exposed.total` is not a count of secured services: it also holds observations not
|
|
77
|
+
yet verified, whose hostname no longer resolves, or whose service no longer answers openly.
|
|
78
|
+
|
|
79
|
+
The tool relays no number it cannot label. A stats field this version does not know is left
|
|
80
|
+
out, and the note names it; so is a count in an unexpected shape. A ranked row carries only
|
|
81
|
+
its name and its count.
|
|
62
82
|
|
|
63
83
|
## What a result means
|
|
64
84
|
|
|
@@ -68,9 +88,11 @@ outage for an all-clear:
|
|
|
68
88
|
- **Success** — the first text block is the API's JSON verbatim; the second is a one-line
|
|
69
89
|
note saying the call succeeded, which base URL answered, and what it found. When the
|
|
70
90
|
feed genuinely holds nothing for the query the note says so in words ("we looked and
|
|
71
|
-
found nothing … not a lookup failure"), because a measured zero is a measurement.
|
|
72
|
-
exception to verbatim
|
|
73
|
-
|
|
91
|
+
found nothing … not a lookup failure"), because a measured zero is a measurement. The
|
|
92
|
+
exception to verbatim is `exposure_radar`'s `shadow_ai`: its counts are regrouped as above,
|
|
93
|
+
and its `poller` block carries only `running` and `last_run_at`, or is dropped when it
|
|
94
|
+
carries no real completion time. The block's other fields describe the server instance that
|
|
95
|
+
answered, not the radar. Its `last_run_at` is when the radar's leader last
|
|
74
96
|
completed a Certificate Transparency (crt.sh) cycle, and its `running` is true only when
|
|
75
97
|
that was within 30 minutes of the answer. When `running` is true, the note says the
|
|
76
98
|
radar's leader last completed a cycle at that time; when it is false, the note says no
|
package/dist/index.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// Result contract (#1874). A tool answers in exactly one of two shapes, and they never blur:
|
|
11
11
|
// - success: content[0] is the API's JSON verbatim; content[1] is a one-line note saying
|
|
12
12
|
// the call succeeded, where, and what it found — in words, including when it found
|
|
13
|
-
// nothing ("we looked and found nothing" is a measurement).
|
|
13
|
+
// nothing ("we looked and found nothing" is a measurement). One exception: exposure_radar
|
|
14
|
+
// regroups the shadow-AI answer by what each number counts (readShadowAI, #2307).
|
|
14
15
|
// - failure (isError: true): the lookup did not complete — unreachable host, non-2xx,
|
|
15
16
|
// timeout, or a 2xx whose body is not a JSON object — named by tool, cause and base URL.
|
|
16
17
|
// Never a success with null fields: a model handed one of those tells its user "no
|
|
@@ -388,13 +389,26 @@ const RADARS = [
|
|
|
388
389
|
["leaked_credentials", "/api/v1/public/leaked-credentials/stats"],
|
|
389
390
|
["shadow_ai", "/api/v1/public/shadow-ai-radar/stats"],
|
|
390
391
|
];
|
|
391
|
-
// The shadow-AI stats answer,
|
|
392
|
+
// The shadow-AI stats answer, regrouped by what each number counts (#2307).
|
|
392
393
|
//
|
|
393
|
-
// stats
|
|
394
|
-
//
|
|
395
|
-
//
|
|
394
|
+
// GET /api/v1/public/shadow-ai-radar/stats (core-backend shadowctlog store.go Stats) mixes
|
|
395
|
+
// three kinds of number, and only one of them counts exposed services:
|
|
396
|
+
// - confirmed exposed: rows whose liveness is active or rechecking, the published exposure
|
|
397
|
+
// set (visible_by_category, last_24h_visible_count);
|
|
398
|
+
// - observed: every Certificate Transparency or Shodan observation on record, whatever its
|
|
399
|
+
// verification state (total, by_category, last_24h_count, trend_30d, and the top_products,
|
|
400
|
+
// top_countries and top_issuers rankings);
|
|
401
|
+
// - authentication state: rows where a probe observed an authentication gate (auth_confirmed,
|
|
402
|
+
// liveness authenticated) or could not tell (auth_undetermined, liveness inconclusive).
|
|
403
|
+
// On 2026-09-27 total was 36,222 against 2,000 confirmed exposed, and top_products ranked
|
|
404
|
+
// LiteLLM at 6,813 observations while 1,395 LLM-PROXY services were confirmed exposed. Relayed
|
|
405
|
+
// under the API's names, every one of those reads as an exposure count. So the tool relays them
|
|
406
|
+
// grouped as confirmed_exposed, observed and authentication, with the note labelling each, and
|
|
407
|
+
// relays nothing it cannot label: a stats field this version does not know, a field other than
|
|
408
|
+
// a ranked row's label and count, and the poller block's instance fields are left out, the
|
|
409
|
+
// first named in the note.
|
|
396
410
|
//
|
|
397
|
-
//
|
|
411
|
+
// The `poller` block's running and last_run_at describe the radar's fleet, not the instance
|
|
398
412
|
// that answered (core-backend shadowctlog Poller.Status): last_run_at is when the fleet's
|
|
399
413
|
// leader last COMPLETED a crt.sh (Certificate Transparency) cycle, and running is true only
|
|
400
414
|
// when that was within pollerSilentAfter, 30 minutes, of the answer. The API sends both or
|
|
@@ -402,52 +416,187 @@ const RADARS = [
|
|
|
402
416
|
// than that answered from the serving instance's own memory, so a follower sent running:false
|
|
403
417
|
// and last_run_at 0001-01-01T00:00:00Z, which reads as "the radar is down and has never run".
|
|
404
418
|
// So the note reads three cases: running with a real completion time, stopped since a real
|
|
405
|
-
// completion time, and unknown. Only the first two are relayed
|
|
406
|
-
// completion time and running verdict) is dropped, so no running:false
|
|
407
|
-
// the model as freshness.
|
|
419
|
+
// completion time, and unknown. Only the first two are relayed, as those two fields alone; an
|
|
420
|
+
// unknown block (no real completion time and running verdict) is dropped, so no running:false
|
|
421
|
+
// or zero time reaches the model as freshness. The block's other fields (interval_seconds,
|
|
422
|
+
// shodan_enabled, and a polling instance's last_new_inserts, skipped_as_follower,
|
|
423
|
+
// consecutive_fails and shodan_last_new) describe the instance that answered, not the fleet,
|
|
424
|
+
// and none of them counts AI services.
|
|
408
425
|
const SHADOW_AI_SILENT_AFTER = "30 minutes";
|
|
426
|
+
// The stats fields readShadowAI knows how to label. Any other is left out and named.
|
|
427
|
+
const SHADOW_AI_STATS = new Set([
|
|
428
|
+
"total",
|
|
429
|
+
"by_category",
|
|
430
|
+
"visible_by_category",
|
|
431
|
+
"last_observation",
|
|
432
|
+
"last_24h_count",
|
|
433
|
+
"last_24h_visible_count",
|
|
434
|
+
"auth_confirmed",
|
|
435
|
+
"auth_undetermined",
|
|
436
|
+
"top_products",
|
|
437
|
+
"top_countries",
|
|
438
|
+
"top_issuers",
|
|
439
|
+
"trend_30d",
|
|
440
|
+
]);
|
|
441
|
+
// The observed rankings and the daily series: API field, and the row field that names each row.
|
|
442
|
+
const SHADOW_AI_ROWS = [
|
|
443
|
+
["trend_30d", "date"],
|
|
444
|
+
["top_products", "product"],
|
|
445
|
+
["top_countries", "country"],
|
|
446
|
+
["top_issuers", "issuer"],
|
|
447
|
+
];
|
|
448
|
+
const isCount = (v) => typeof v === "number" && Number.isFinite(v);
|
|
449
|
+
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
450
|
+
// A category → count map, or nothing unless every value is a count: a partial map is an
|
|
451
|
+
// undercount that reads as a measurement (store.go categoryCounts).
|
|
452
|
+
function countMap(v) {
|
|
453
|
+
if (!isPlainObject(v))
|
|
454
|
+
return undefined;
|
|
455
|
+
const entries = Object.entries(v);
|
|
456
|
+
return entries.every(([, n]) => isCount(n)) ? Object.fromEntries(entries) : undefined;
|
|
457
|
+
}
|
|
458
|
+
// A ranking or a daily series, each row cut down to its label and its count, so a field the
|
|
459
|
+
// backend adds to a row later is left out rather than relayed unlabelled. All rows or none.
|
|
460
|
+
function countRows(v, label) {
|
|
461
|
+
if (!Array.isArray(v))
|
|
462
|
+
return undefined;
|
|
463
|
+
const rows = [];
|
|
464
|
+
for (const r of v) {
|
|
465
|
+
const name = field(r, label);
|
|
466
|
+
const count = field(r, "count");
|
|
467
|
+
if (typeof name !== "string" || !isCount(count))
|
|
468
|
+
return undefined;
|
|
469
|
+
rows.push({ [label]: name, count });
|
|
470
|
+
}
|
|
471
|
+
return rows;
|
|
472
|
+
}
|
|
409
473
|
function readShadowAI(sa) {
|
|
410
474
|
const sentences = [];
|
|
411
|
-
const
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
475
|
+
const stats = field(sa, "stats");
|
|
476
|
+
const used = new Set();
|
|
477
|
+
const take = (key, v) => {
|
|
478
|
+
if (v !== undefined)
|
|
479
|
+
used.add(key);
|
|
480
|
+
return v;
|
|
481
|
+
};
|
|
482
|
+
// Confirmed exposed: liveness IN ('active','rechecking') (store.go Stats, visible_by_category
|
|
483
|
+
// and last_24h_visible_count).
|
|
484
|
+
const confirmed = {};
|
|
485
|
+
const visible = take("visible_by_category", countMap(field(stats, "visible_by_category")));
|
|
486
|
+
if (visible) {
|
|
487
|
+
confirmed.total = Object.values(visible).reduce((a, b) => a + b, 0);
|
|
488
|
+
confirmed.by_category = visible;
|
|
489
|
+
}
|
|
490
|
+
const last24Visible = take("last_24h_visible_count", numAt(stats, "last_24h_visible_count"));
|
|
491
|
+
if (last24Visible !== undefined)
|
|
492
|
+
confirmed.last_24h = last24Visible;
|
|
493
|
+
// Observed: every row of shadow_ct_observations, whatever its liveness.
|
|
494
|
+
const observed = {};
|
|
495
|
+
const total = take("total", numAt(stats, "total"));
|
|
496
|
+
if (total !== undefined)
|
|
497
|
+
observed.total = total;
|
|
498
|
+
const byCategory = take("by_category", countMap(field(stats, "by_category")));
|
|
499
|
+
if (byCategory)
|
|
500
|
+
observed.by_category = byCategory;
|
|
501
|
+
const last24 = take("last_24h_count", numAt(stats, "last_24h_count"));
|
|
502
|
+
if (last24 !== undefined)
|
|
503
|
+
observed.last_24h = last24;
|
|
504
|
+
const rows = {};
|
|
505
|
+
for (const [key, label] of SHADOW_AI_ROWS) {
|
|
506
|
+
const r = take(key, countRows(field(stats, key), label));
|
|
507
|
+
if (r)
|
|
508
|
+
observed[key] = rows[key] = r;
|
|
509
|
+
}
|
|
510
|
+
// A timestamp, not a count; one that is not a real instant is dropped (see realInstant).
|
|
511
|
+
const lastObservation = take("last_observation", strAt(stats, "last_observation"));
|
|
512
|
+
if (realInstant(lastObservation))
|
|
513
|
+
observed.last_observation = lastObservation;
|
|
514
|
+
// Authentication state: liveness 'authenticated' and 'inconclusive'.
|
|
515
|
+
const authentication = {};
|
|
516
|
+
const authSeen = take("auth_confirmed", numAt(stats, "auth_confirmed"));
|
|
517
|
+
if (authSeen !== undefined)
|
|
518
|
+
authentication.observed = authSeen;
|
|
519
|
+
const authUnknown = take("auth_undetermined", numAt(stats, "auth_undetermined"));
|
|
520
|
+
if (authUnknown !== undefined)
|
|
521
|
+
authentication.not_determined = authUnknown;
|
|
522
|
+
sentences.push(confirmed.total === undefined
|
|
523
|
+
? `Shadow AI: the answer carries no usable stats.visible_by_category, so it does not say how many services are confirmed exposed${last24Visible === undefined ? "" : `; confirmed_exposed.last_24h (${last24Visible}) counts confirmed-exposed services first recorded in the last 24 h`}.`
|
|
524
|
+
: `Shadow AI confirmed exposed: ${confirmed.total} (confirmed_exposed.total, the sum of confirmed_exposed.by_category: services EchelonGraph's probes found answering without an authentication gate, with liveness active or, during a re-check, rechecking)${last24Visible === undefined ? "" : `, ${last24Visible} of them first recorded in the last 24 h (confirmed_exposed.last_24h)`}. Only confirmed_exposed counts exposed services.`);
|
|
424
525
|
if (total !== undefined) {
|
|
425
|
-
sentences.push(`
|
|
526
|
+
sentences.push(`Shadow AI observed: ${total} (observed.total) counts every Certificate Transparency or Shodan observation on record, whatever its verification state: that many observed, not that many exposed.`);
|
|
426
527
|
}
|
|
427
|
-
|
|
528
|
+
const first = (key, label) => {
|
|
529
|
+
const top = rows[key]?.[0];
|
|
530
|
+
return top === undefined ? "" : ` (first: ${top[label]}, ${top.count} observations)`;
|
|
531
|
+
};
|
|
532
|
+
const observedParts = [
|
|
533
|
+
byCategory && "observed.by_category counts them by category",
|
|
534
|
+
last24 !== undefined && `observed.last_24h (${last24}) counts those first recorded in the last 24 h`,
|
|
535
|
+
rows.trend_30d && "observed.trend_30d counts them per UTC day over the last 30 days",
|
|
536
|
+
rows.top_products && `observed.top_products ranks up to ten products by observations${first("top_products", "product")}`,
|
|
537
|
+
rows.top_countries && `observed.top_countries ranks up to ten countries by observations${first("top_countries", "country")}`,
|
|
538
|
+
rows.top_issuers &&
|
|
539
|
+
`observed.top_issuers ranks up to ten issuers by observations, an issuer being the certificate's CA for a Certificate Transparency observation and the hosting operator Shodan reports for a Shodan one${first("top_issuers", "issuer")}`,
|
|
540
|
+
].filter((p) => typeof p === "string");
|
|
541
|
+
if (observedParts.length) {
|
|
542
|
+
sentences.push(`${total === undefined ? "Every number" : "Every other number"} under observed counts the same observations, not exposed services: ${observedParts.join("; ")}.`);
|
|
543
|
+
}
|
|
544
|
+
const authParts = [
|
|
545
|
+
authSeen !== undefined &&
|
|
546
|
+
`authentication.observed (${authSeen}) counts observations where a probe observed an authentication gate (a 401/403, a login page or an auth marker; liveness authenticated)`,
|
|
547
|
+
authUnknown !== undefined &&
|
|
548
|
+
`authentication.not_determined (${authUnknown}) counts observations whose service answered but where no probe could tell whether it enforces authentication (liveness inconclusive)`,
|
|
549
|
+
].filter((p) => typeof p === "string");
|
|
550
|
+
if (authParts.length) {
|
|
551
|
+
sentences.push(`Shadow AI authentication, over every observation on record: ${authParts.join("; ")}.`);
|
|
552
|
+
sentences.push("Neither authentication count is part of confirmed_exposed, and observed.total minus confirmed_exposed.total is not a count of secured services: it also holds observations not yet verified (liveness unverified), whose hostname no longer resolves (unreachable), or whose service no longer answers openly (resolved).");
|
|
553
|
+
}
|
|
554
|
+
// What the answer carried that the tool cannot label: named, and left out.
|
|
555
|
+
const unknown = [
|
|
556
|
+
...Object.keys(isPlainObject(sa) ? sa : {}).filter((k) => k !== "stats" && k !== "poller"),
|
|
557
|
+
...Object.keys(isPlainObject(stats) ? stats : {})
|
|
558
|
+
.filter((k) => !SHADOW_AI_STATS.has(k))
|
|
559
|
+
.map((k) => `stats.${k}`),
|
|
560
|
+
];
|
|
561
|
+
const unusable = Object.entries(isPlainObject(stats) ? stats : {})
|
|
562
|
+
.filter(([k, v]) => SHADOW_AI_STATS.has(k) && !used.has(k) && v !== null)
|
|
563
|
+
.map(([k]) => `stats.${k}`);
|
|
564
|
+
if (!isPlainObject(stats))
|
|
565
|
+
sentences.push("The answer carries no stats object, so it gives no shadow-AI counts.");
|
|
566
|
+
if (unknown.length) {
|
|
567
|
+
sentences.push(`Left out of shadow_ai because this version of the tool cannot label them: ${unknown.join(", ")}.`);
|
|
568
|
+
}
|
|
569
|
+
if (unusable.length) {
|
|
570
|
+
sentences.push(`Left out of shadow_ai because they were not in the expected shape: ${unusable.join(", ")}.`);
|
|
571
|
+
}
|
|
572
|
+
const data = {};
|
|
573
|
+
if (Object.keys(confirmed).length)
|
|
574
|
+
data.confirmed_exposed = confirmed;
|
|
575
|
+
if (Object.keys(observed).length)
|
|
576
|
+
data.observed = observed;
|
|
577
|
+
if (Object.keys(authentication).length)
|
|
578
|
+
data.authentication = authentication;
|
|
428
579
|
const lastRun = strAt(sa, "poller", "last_run_at");
|
|
429
580
|
const running = boolAt(sa, "poller", "running");
|
|
430
581
|
if (realInstant(lastRun) && running === true) {
|
|
582
|
+
data.poller = { running, last_run_at: lastRun };
|
|
431
583
|
sentences.push(`Shadow-AI radar freshness: the radar's leader last completed a Certificate Transparency (crt.sh) discovery cycle at ${lastRun} (poller.running is true: that was within ${SHADOW_AI_SILENT_AFTER} of the API's answer).`);
|
|
432
584
|
}
|
|
433
585
|
else if (realInstant(lastRun) && running === false) {
|
|
586
|
+
data.poller = { running, last_run_at: lastRun };
|
|
434
587
|
sentences.push(`Shadow-AI radar freshness: no Certificate Transparency (crt.sh) discovery cycle has completed since ${lastRun}, when the radar's leader last completed one (poller.running is false: that was more than ${SHADOW_AI_SILENT_AFTER} before the API's answer), so the shadow-AI figures may be stale.`);
|
|
435
588
|
}
|
|
436
589
|
else if (field(sa, "poller") === undefined) {
|
|
437
590
|
sentences.push("Shadow-AI radar freshness is unknown: the answer carries no poller block.");
|
|
438
591
|
}
|
|
439
592
|
else {
|
|
440
|
-
const rest = { ...sa };
|
|
441
|
-
delete rest.poller;
|
|
442
|
-
data = rest;
|
|
443
593
|
sentences.push("Shadow-AI radar freshness is unknown: the answer's poller block does not give both a real time at which the radar's leader last completed a cycle (poller.last_run_at) and a running verdict (poller.running), so that block was left out of the data above.");
|
|
444
594
|
}
|
|
445
|
-
const lastObservation = strAt(sa, "stats", "last_observation");
|
|
446
595
|
if (realInstant(lastObservation))
|
|
447
596
|
sentences.push(`Latest shadow-AI observation on record: ${lastObservation}.`);
|
|
448
597
|
return { data, sentences };
|
|
449
598
|
}
|
|
450
|
-
server.tool("exposure_radar", `Aggregate totals from EchelonGraph's internet-exposure radars, each refreshed on its own schedule: shadow AI services found through Certificate Transparency logs and Shodan and then checked by EchelonGraph's identified probes; internet-facing services running actively-exploited (CISA-KEV) CVEs, plus the ransomware-linked subset, derived from Shodan data; unauthenticated data stores and observability UIs, found through Shodan (LeakIX when Shodan query credits run low) and then confirmed by EchelonGraph's own identified check (not a pure read: on Redis it names its client, and on ClickHouse its query is recorded in the server's query log); and leaked credentials sampled from public GitHub push events. The kev_exposure and exposed_databases distinct_hosts figures count distinct ip:port services, so a machine answering on two ports counts twice.
|
|
599
|
+
server.tool("exposure_radar", `Aggregate totals from EchelonGraph's internet-exposure radars, each refreshed on its own schedule: shadow AI services found through Certificate Transparency logs and Shodan and then checked by EchelonGraph's identified probes; internet-facing services running actively-exploited (CISA-KEV) CVEs, plus the ransomware-linked subset, derived from Shodan data; unauthenticated data stores and observability UIs, found through Shodan (LeakIX when Shodan query credits run low) and then confirmed by EchelonGraph's own identified check (not a pure read: on Redis it names its client, and on ClickHouse its query is recorded in the server's query log); and leaked credentials sampled from public GitHub push events. The kev_exposure and exposed_databases distinct_hosts figures count distinct ip:port services, so a machine answering on two ports counts twice. The shadow_ai result is regrouped by what each number counts, and carries no number the tool cannot label. shadow_ai.confirmed_exposed counts services EchelonGraph's probes found answering without an authentication gate (liveness active, or rechecking during a re-check): confirmed_exposed.total is the sum of confirmed_exposed.by_category, and confirmed_exposed.last_24h counts those first recorded in the last 24 h. Only confirmed_exposed counts exposed services. shadow_ai.observed counts every Certificate Transparency or Shodan observation on record, whatever its verification state: its numbers are observed, not exposed. They are observed.total; observed.by_category (the same observations by category); observed.last_24h (those first recorded in the last 24 h); observed.trend_30d (observations per UTC day over the last 30 days); and observed.top_products, observed.top_countries and observed.top_issuers (up to ten products, countries and issuers ranked by observations, where an issuer is the certificate's CA for a Certificate Transparency observation and the hosting operator Shodan reports for a Shodan one). shadow_ai.authentication counts observations by probe outcome: authentication.observed where a probe observed an authentication gate (a 401/403, a login page or an auth marker), and authentication.not_determined where the service answered but no probe could tell. Neither authentication count is part of confirmed_exposed, and observed.total minus confirmed_exposed.total is not a count of secured services. The shadow-AI poller block carries only running and last_run_at: last_run_at is when the radar's leader last completed a Certificate Transparency (crt.sh) cycle, and its running is true only when that was within ${SHADOW_AI_SILENT_AFTER} of the answer. ${SHODAN_OWNERSHIP}`, {}, async () => {
|
|
451
600
|
const tool = "exposure_radar";
|
|
452
601
|
try {
|
|
453
602
|
const results = await Promise.all(RADARS.map(([, path]) => api(path)));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "echelongraph-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"mcpName": "io.echelongraph/echelongraph-mcp",
|
|
5
5
|
"description": "EchelonGraph CVE and internet-exposure data as an MCP server: CVEs with EchelonGraph multi-source scores, CISA-KEV and EPSS, plus a per-CVE internet-exposure footprint derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan). Free and keyless, for Claude, Cursor and any MCP client.",
|
|
6
6
|
"type": "module",
|
package/server.json
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
"title": "EchelonGraph CVE & Exposure",
|
|
5
5
|
"description": "CVEs (NVD, CISA-KEV, EPSS, GHSA) and per-CVE exposure from Shodan data (© Shodan). Free, keyless.",
|
|
6
6
|
"websiteUrl": "https://echelongraph.io/pulse/mcp",
|
|
7
|
-
"version": "1.0.
|
|
7
|
+
"version": "1.0.3",
|
|
8
8
|
"packages": [
|
|
9
9
|
{
|
|
10
10
|
"registryType": "npm",
|
|
11
11
|
"identifier": "echelongraph-mcp",
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.3",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
},
|