echelongraph-mcp 1.0.1 → 1.0.2
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 +19 -5
- package/dist/index.js +38 -19
- package/package.json +2 -2
- package/server.json +3 -3
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ It exposes EchelonGraph's CVE Pulse (NVD + MITRE-CNA *pre-NVD* + CISA-KEV + EPSS
|
|
|
8
8
|
GHSA, fused into one score) **plus a per-CVE internet-exposure footprint**: how many
|
|
9
9
|
internet-facing services (distinct ip:port) EchelonGraph's KEV-exposure radar has on record
|
|
10
10
|
running a version that maps to the CVE. Exposure counts are derived from Shodan data.
|
|
11
|
+
Shodan data is owned by Shodan, which holds its copyright (© Shodan).
|
|
11
12
|
|
|
12
13
|
Free and keyless: no API key, no auth, read-only. The server makes no request other than the
|
|
13
14
|
API call a tool needs to answer.
|
|
@@ -20,7 +21,7 @@ API call a tool needs to answer.
|
|
|
20
21
|
| `search_cves` | Search/filter CVEs (severity, min CVSS, text, sort) with EchelonGraph scores. |
|
|
21
22
|
| `get_cve` | Full detail for one CVE (CVSS v3/v4, EG score, EPSS, KEV+ransomware, GHSA, CWE, references). |
|
|
22
23
|
| `cve_exposure` | Internet-exposure footprint for a CVE: exposed service count (distinct ip:port, the `exposed_hosts` field) + country/product breakdown, from the KEV-exposure radar. |
|
|
23
|
-
| `exposure_radar` | Aggregate totals across the exposure radars: shadow AI; services running CISA-KEV CVEs; unauthenticated data stores and observability UIs, found through Shodan (LeakIX when Shodan query credits run low) and then confirmed by EchelonGraph's own read
|
|
24
|
+
| `exposure_radar` | Aggregate totals across the exposure radars: shadow AI; services running CISA-KEV CVEs; 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, which is not a pure read (on Redis it names its client; on ClickHouse its query lands in the server's query log); leaked credentials. |
|
|
24
25
|
|
|
25
26
|
Every figure is as fresh as the schedule that refreshes it. The CVE feed is polled from its
|
|
26
27
|
sources on a schedule; each radar refreshes on its own.
|
|
@@ -69,9 +70,15 @@ outage for an all-clear:
|
|
|
69
70
|
feed genuinely holds nothing for the query the note says so in words ("we looked and
|
|
70
71
|
found nothing … not a lookup failure"), because a measured zero is a measurement. One
|
|
71
72
|
exception to verbatim: `exposure_radar` drops the shadow-AI `poller` block when it carries
|
|
72
|
-
no real
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
no real completion time. That block's `last_run_at` is when the radar's leader last
|
|
74
|
+
completed a Certificate Transparency (crt.sh) cycle, and its `running` is true only when
|
|
75
|
+
that was within 30 minutes of the answer. When `running` is true, the note says the
|
|
76
|
+
radar's leader last completed a cycle at that time; when it is false, the note says no
|
|
77
|
+
cycle has completed since that time; when `last_run_at` is absent, the note says the
|
|
78
|
+
radar's freshness is unknown and the block is left out. An older API answered from
|
|
79
|
+
whichever server instance served the request, and a follower instance sent
|
|
80
|
+
`running: false` with the zero time `0001-01-01T00:00:00Z`; that too is unknown freshness,
|
|
81
|
+
never presented as a stopped radar.
|
|
75
82
|
- **Failure** — an MCP error result (`isError: true`) whenever the lookup did not complete:
|
|
76
83
|
the host could not be reached, it answered non-2xx, it took longer than the timeout, or
|
|
77
84
|
it answered 2xx with a body that is not a JSON object. The text names the tool, the
|
|
@@ -136,4 +143,11 @@ counted as external MCP adoption. `npm test` never leaves the machine.
|
|
|
136
143
|
|
|
137
144
|
## License
|
|
138
145
|
|
|
139
|
-
|
|
146
|
+
The code is MIT-licensed.
|
|
147
|
+
|
|
148
|
+
The CVE Pulse compilation (how EchelonGraph combines its CVE sources, and the EchelonGraph
|
|
149
|
+
score) is © EchelonGraph, served under the CVE Pulse free-access terms; the source records it
|
|
150
|
+
compiles stay under their publishers' terms.
|
|
151
|
+
|
|
152
|
+
Exposure counts are derived from Shodan data. Shodan data is owned by Shodan, which holds its
|
|
153
|
+
copyright (© Shodan). EchelonGraph claims no ownership of it or copyright in it.
|
package/dist/index.js
CHANGED
|
@@ -308,12 +308,18 @@ const CVE_ID = /^CVE-\d{4}-\d{4,}$/i;
|
|
|
308
308
|
// Quoted when the API does not send its own `method` string.
|
|
309
309
|
const EXPOSURE_METHOD = "Shodan banner match on the radar's tracked products; up to 100 ip:port services per product query; searched every 12 h when Shodan query credits allow";
|
|
310
310
|
const SHODAN_ATTRIBUTION = "Exposure counts are derived from Shodan data.";
|
|
311
|
+
// Shodan's terms (https://static.shodan.io/legal/terms.html, read 2026-09-27) ask for two
|
|
312
|
+
// things: "You must attribute such usage to Shodan", and materials "referencing, including or
|
|
313
|
+
// otherwise based on Shodan information or materials, must clearly indicate Shodan's ownership
|
|
314
|
+
// and copyright in the applicable Shodan materials" (#2306). The attribution above does the
|
|
315
|
+
// first; this sentence, placed beside it wherever Shodan data is named, does the second.
|
|
316
|
+
const SHODAN_OWNERSHIP = "Shodan data is owned by Shodan, which holds its copyright (© Shodan).";
|
|
311
317
|
function exposureNote(head, id, d) {
|
|
312
318
|
const cve = strAt(d, "cve_id") ?? id;
|
|
313
319
|
const hosts = numAt(d, "exposed_hosts");
|
|
314
320
|
const tracked = boolAt(d, "tracked");
|
|
315
321
|
// The API's own method sentence may end in a full stop; the note supplies one.
|
|
316
|
-
const how = `Method: ${(strAt(d, "method") ?? EXPOSURE_METHOD).replace(/[.\s]+$/, "")}. ${SHODAN_ATTRIBUTION}`;
|
|
322
|
+
const how = `Method: ${(strAt(d, "method") ?? EXPOSURE_METHOD).replace(/[.\s]+$/, "")}. ${SHODAN_ATTRIBUTION} ${SHODAN_OWNERSHIP}`;
|
|
317
323
|
// Before kev_catalog_listed existed, kev_listed was bool_or over the radar's observations:
|
|
318
324
|
// false for every CVE the radar has no host for, whatever the CISA-KEV catalog says.
|
|
319
325
|
// kev_catalog_listed is cves.kev_listed, the flag the KEV fetcher sets on EchelonGraph's CVE
|
|
@@ -350,7 +356,7 @@ function exposureNote(head, id, d) {
|
|
|
350
356
|
: "";
|
|
351
357
|
return `${head} (state: exposed) The radar has ${hosts} internet-facing services (distinct ip:port, the exposed_hosts field) on record whose banner version maps to ${cve}${countries === undefined ? "" : ` across ${countries} countries`}.${seen} ${kev} ${how}`;
|
|
352
358
|
}
|
|
353
|
-
server.tool("cve_exposure",
|
|
359
|
+
server.tool("cve_exposure", `Internet-exposure footprint for one CVE from EchelonGraph's KEV-exposure radar: how many internet-facing services (distinct ip:port, returned as exposed_hosts; a machine answering on two ports counts twice) the radar has on record running a version its CVE matcher maps to this CVE, with a country/product breakdown and a ransomware flag. Aggregate and host-redacted; free and keyless. Method: exposure counts are derived from Shodan data. ${SHODAN_OWNERSHIP} Every 12 h, when Shodan query credits allow, the radar runs one Shodan query per tracked product, reads up to 100 ip:port services per query, and keeps a service when its banner version matches a CISA-KEV or high-EPSS CVE; a service not seen on its port for 21 days is dropped. last_seen is when a service was last seen listening on its port, not when its vulnerable version was last confirmed: between searches a re-check that finds the port still listed refreshes it without re-reading the banner, so a patched service can stay counted while its port stays open. A count is therefore a banner-version inference over a sample, not an exploit test and not an internet-wide census. The radar only looks for its tracked set of CVEs: for a CVE outside that set the result says NOT ASSESSED, and its 0 is not a measurement.`, { cve_id: z.string().describe("a CVE ID, e.g. CVE-2023-44487") }, async ({ cve_id }) => {
|
|
354
360
|
const tool = "cve_exposure";
|
|
355
361
|
const id = cve_id.trim();
|
|
356
362
|
if (!id)
|
|
@@ -388,10 +394,18 @@ const RADARS = [
|
|
|
388
394
|
// 2026-09-26); the confirmed-exposed count is the sum of stats.visible_by_category (1,983
|
|
389
395
|
// that day). Only the second may be called exposed.
|
|
390
396
|
//
|
|
391
|
-
// Its `poller` block
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
397
|
+
// Its `poller` block's running and last_run_at describe the radar's fleet, not the instance
|
|
398
|
+
// that answered (core-backend shadowctlog Poller.Status): last_run_at is when the fleet's
|
|
399
|
+
// leader last COMPLETED a crt.sh (Certificate Transparency) cycle, and running is true only
|
|
400
|
+
// when that was within pollerSilentAfter, 30 minutes, of the answer. The API sends both or
|
|
401
|
+
// neither, and omits both when it cannot tell; it never sends the Go zero time. An API older
|
|
402
|
+
// than that answered from the serving instance's own memory, so a follower sent running:false
|
|
403
|
+
// and last_run_at 0001-01-01T00:00:00Z, which reads as "the radar is down and has never run".
|
|
404
|
+
// 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; an unknown block (no real
|
|
406
|
+
// completion time and running verdict) is dropped, so no running:false or zero time reaches
|
|
407
|
+
// the model as freshness.
|
|
408
|
+
const SHADOW_AI_SILENT_AFTER = "30 minutes";
|
|
395
409
|
function readShadowAI(sa) {
|
|
396
410
|
const sentences = [];
|
|
397
411
|
const byCategory = field(sa, "stats", "visible_by_category");
|
|
@@ -411,24 +425,29 @@ function readShadowAI(sa) {
|
|
|
411
425
|
sentences.push(`stats.total (${total}) counts every observation whatever its verification state: that many observed, not that many exposed.`);
|
|
412
426
|
}
|
|
413
427
|
let data = sa;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
428
|
+
const lastRun = strAt(sa, "poller", "last_run_at");
|
|
429
|
+
const running = boolAt(sa, "poller", "running");
|
|
430
|
+
if (realInstant(lastRun) && running === true) {
|
|
431
|
+
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
|
+
}
|
|
433
|
+
else if (realInstant(lastRun) && running === false) {
|
|
434
|
+
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
|
+
}
|
|
436
|
+
else if (field(sa, "poller") === undefined) {
|
|
437
|
+
sentences.push("Shadow-AI radar freshness is unknown: the answer carries no poller block.");
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
const rest = { ...sa };
|
|
441
|
+
delete rest.poller;
|
|
442
|
+
data = rest;
|
|
443
|
+
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.");
|
|
425
444
|
}
|
|
426
445
|
const lastObservation = strAt(sa, "stats", "last_observation");
|
|
427
446
|
if (realInstant(lastObservation))
|
|
428
447
|
sentences.push(`Latest shadow-AI observation on record: ${lastObservation}.`);
|
|
429
448
|
return { data, sentences };
|
|
430
449
|
}
|
|
431
|
-
server.tool("exposure_radar",
|
|
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. For shadow AI, stats.total counts every observation whatever its verification state (observed, not exposed); the confirmed-exposed count is the sum of stats.visible_by_category, and last_24h_visible_count is its last-24-hour slice. The shadow-AI poller block's 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 () => {
|
|
432
451
|
const tool = "exposure_radar";
|
|
433
452
|
try {
|
|
434
453
|
const results = await Promise.all(RADARS.map(([, path]) => api(path)));
|
|
@@ -456,7 +475,7 @@ server.tool("exposure_radar", "Aggregate totals from EchelonGraph's internet-exp
|
|
|
456
475
|
}
|
|
457
476
|
const shadow = readShadowAI(data.shadow_ai);
|
|
458
477
|
data.shadow_ai = shadow.data;
|
|
459
|
-
return succeeded(data, `${okHead(tool)} All ${RADARS.length} radars answered: ${RADARS.map(([name]) => name).join(", ")}. ${shadow.sentences.join(" ")} ${RADAR_SOURCES} Each radar refreshes on its own schedule.`);
|
|
478
|
+
return succeeded(data, `${okHead(tool)} All ${RADARS.length} radars answered: ${RADARS.map(([name]) => name).join(", ")}. ${shadow.sentences.join(" ")} ${RADAR_SOURCES} ${SHODAN_OWNERSHIP} Each radar refreshes on its own schedule.`);
|
|
460
479
|
}
|
|
461
480
|
catch (e) {
|
|
462
481
|
return crashed(tool, e);
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "echelongraph-mcp",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"mcpName": "io.echelongraph/echelongraph-mcp",
|
|
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. Free and keyless, for Claude, Cursor and any MCP client.",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"echelongraph-mcp": "dist/index.js"
|
package/server.json
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.echelongraph/echelongraph-mcp",
|
|
4
4
|
"title": "EchelonGraph CVE & Exposure",
|
|
5
|
-
"description": "
|
|
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.2",
|
|
8
8
|
"packages": [
|
|
9
9
|
{
|
|
10
10
|
"registryType": "npm",
|
|
11
11
|
"identifier": "echelongraph-mcp",
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.2",
|
|
13
13
|
"transport": {
|
|
14
14
|
"type": "stdio"
|
|
15
15
|
},
|