echelongraph-mcp 2.7.2 → 3.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/CHANGELOG.md +219 -0
- package/README.md +163 -433
- package/dist/fixStatus.js +30 -0
- package/dist/httpPolicy.js +16 -1
- package/dist/index.js +287 -34
- package/dist/prompts.js +63 -33
- package/dist/requiredText.js +16 -1
- package/dist/resources.js +32 -6
- package/dist/textBudget.js +588 -22
- package/dist/tools/check_affected.js +45 -12
- package/dist/tools/check_sbom.js +158 -9
- package/dist/tools/cve_intel.js +63 -9
- package/dist/tools/cve_remediation.js +230 -16
- package/dist/tools/epss_history.js +10 -4
- package/dist/tools/get_cwe.js +7 -2
- package/dist/tools/kev_recent.js +22 -3
- package/dist/tools/match_batch.js +27 -7
- package/dist/tools/scan_manifest.js +35 -10
- package/dist/tools/vendor_advisories.js +47 -11
- package/docs/exposure-radar.md +181 -0
- package/docs/rate-limits.md +50 -0
- package/docs/structured-results.md +43 -0
- package/docs/tools.md +167 -0
- package/package.json +2 -1
- package/server.json +2 -2
package/docs/tools.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# echelongraph-mcp: how each tool reads its answer
|
|
2
|
+
|
|
3
|
+
Part of the [README](../README.md), whose Tools table says what each tool answers. This page says,
|
|
4
|
+
field by field, how `cve_summary`, `get_cve`, `search_cves`, `cve_exposure` and `check_affected`
|
|
5
|
+
read the API's answer; `exposure_radar` has [a page of its own](exposure-radar.md). It is kept out
|
|
6
|
+
of the README because npm keeps only the first 65,536 characters of a package's README, and the
|
|
7
|
+
package ships it in `docs/`. Exposure counts are derived from Shodan data. Shodan data is owned by
|
|
8
|
+
Shodan, which holds its copyright (© Shodan).
|
|
9
|
+
|
|
10
|
+
## How `cve_summary` reads its counts
|
|
11
|
+
|
|
12
|
+
`summary.critical`, `summary.high`, `summary.medium` and `summary.low` count the active CVEs by
|
|
13
|
+
severity band, and `summary.total` counts them all. `summary.none` counts the active CVEs with no
|
|
14
|
+
severity band from any source: CVEs not yet scored, not CVEs rated None. The answer sends the same
|
|
15
|
+
count again as `summary.unscored`.
|
|
16
|
+
|
|
17
|
+
`summary.nvd_critical`, `summary.nvd_high`, `summary.nvd_medium`, `summary.nvd_low` and
|
|
18
|
+
`summary.nvd_none` count the same active CVEs a second time, by NVD's CVSS severity label (v3.x,
|
|
19
|
+
else v4.0). Before NVD's record arrives, or where it gives none, a pre-NVD label from the CVE.org
|
|
20
|
+
record or a GitHub advisory can stand in. They are provenance, never EchelonGraph's severity band. `summary.nvd_none` counts the active CVEs with no
|
|
21
|
+
Critical, High, Medium or Low label there, and many of those carry an NVD CVSS v2 score instead, so
|
|
22
|
+
it is neither a count of CVEs rated None nor the count of CVEs with no severity, which is
|
|
23
|
+
`summary.none`.
|
|
24
|
+
|
|
25
|
+
`summary.rejected` counts the CVE records rejected (withdrawn) by their numbering authority.
|
|
26
|
+
`summary.total` and the other counts leave them out, and they are withdrawn records, never
|
|
27
|
+
vulnerabilities.
|
|
28
|
+
|
|
29
|
+
`poller` holds the in-memory counters of the NVD poller of the one API instance that answered,
|
|
30
|
+
counted since that instance last started and zeroed on every restart: they describe that instance,
|
|
31
|
+
never the feed's size, intake, reliability or freshness, and the note says so whenever the answer
|
|
32
|
+
carries `poller`. A `poller` field the answer sends in a JSON type other than the one the
|
|
33
|
+
outputSchema describes is left out and named in the note, and a `poller` that is neither a JSON
|
|
34
|
+
object nor null is left out whole; `summary` is relayed either way, so a change to that diagnostic
|
|
35
|
+
block never withholds the counts.
|
|
36
|
+
|
|
37
|
+
The tool relays the API's JSON as it was sent, less a `poller` field left out as above. When
|
|
38
|
+
`summary.none`, `summary.nvd_none` or `summary.rejected` is above zero, the note says what it
|
|
39
|
+
counts, with the number from the answer.
|
|
40
|
+
The note says the five NVD counts add up to `summary.total` only when the answer's do, and says
|
|
41
|
+
nothing about a field the answer does not carry.
|
|
42
|
+
|
|
43
|
+
## How `get_cve` and `search_cves` read the EchelonGraph score
|
|
44
|
+
|
|
45
|
+
`echelongraph_score` (0 to 10), its band `echelongraph_severity` and the risk priority
|
|
46
|
+
`echelongraph_risk` (0 to 100) are EchelonGraph's score only when the record's
|
|
47
|
+
`score_assessed` is `true`. A CVE EchelonGraph has not scored carries `score_assessed: false`,
|
|
48
|
+
and it is **not yet scored**, not scored 0. The API leaves those three fields out on it, or (an
|
|
49
|
+
API before that change) sends `0`, `NONE` and `0` as placeholders, which are not a rating and do
|
|
50
|
+
not mean the CVE is harmless. Its `score_confidence` is `NONE`, and `score_unassessed_reason`
|
|
51
|
+
says why: no source has yet published severity data EchelonGraph can score, or the record was
|
|
52
|
+
rejected (withdrawn) by its numbering authority, and a rejected record is never scored.
|
|
53
|
+
|
|
54
|
+
Both tools relay the API's JSON as it was sent, and the note says what the score is, for
|
|
55
|
+
`get_cve` per CVE and for `search_cves` per row, naming each CVE it is about:
|
|
56
|
+
|
|
57
|
+
| API answer | What the note says |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `score_assessed: true` | Nothing more: the score is a score. |
|
|
60
|
+
| `score_assessed: false` | **NOT YET SCORED**, tagged "(score_assessed: false)": report the CVE as not yet scored, never as a zero or low score. Each of `echelongraph_score`, `echelongraph_severity` and `echelongraph_risk` it carries is named as a placeholder. A rejected record is **NOT SCORED** instead. |
|
|
61
|
+
| no `score_assessed` field | The answer does not say whether the CVE was scored (an API older than the field), and a zero `echelongraph_score` in it is not a rating. |
|
|
62
|
+
|
|
63
|
+
## What `patch_available` says
|
|
64
|
+
|
|
65
|
+
`patch_available` is `true` when EchelonGraph holds, or once held, evidence of a fix for the CVE,
|
|
66
|
+
and `patch_evidence` lists the sources it holds now, strongest first: `vendor_patch` (a vendor
|
|
67
|
+
patch on record, from Ubuntu or Red Hat), `fixed_version` (a fixed version recorded for an
|
|
68
|
+
affected package, or the fixed bound of an affected version range), `release_reference` (a
|
|
69
|
+
reference that EchelonGraph confirmed through the GitHub API is a published release containing
|
|
70
|
+
the fix, or a pull request or commit that a published release contains), `vendor_advisory_fix` (a
|
|
71
|
+
vendor advisory that lists a fix for the CVE under the vendor's own structured category: Red Hat's
|
|
72
|
+
CSAF `vendor_fix`, Microsoft's Vendor Fix), `nvd_patch_reference` (an NVD reference tagged Patch),
|
|
73
|
+
`cna_patch_reference` (a reference the CVE's CNA tagged `patch` in its CVE record), `cna_solution`
|
|
74
|
+
(a sentence of the CVE record's solutions, written by its CNA, that names a fixed version) and
|
|
75
|
+
`description_fix` (a sentence of the CVE description that names a fixed version). A reference page
|
|
76
|
+
that only mentions a patch, and a CVE's presence in CISA's KEV catalog, are not evidence.
|
|
77
|
+
`get_cve` relays the CNA's own sentence as `cna_solution`, which section of the record it is as
|
|
78
|
+
`cna_solution_section` (`solution` or `workaround`: a workaround's sentence is the record's
|
|
79
|
+
workaround, never its solution), its date for when the vulnerability was made public as
|
|
80
|
+
`date_public` (with `date_public_source` `cna`), and the CNA as `cna`, each only where the CVE
|
|
81
|
+
record has it. `false` means no fix evidence is on record, which is **not** a
|
|
82
|
+
finding that no fix exists. Once `true`, `patch_available` is not set back to `false`, so it can be
|
|
83
|
+
`true` beside an empty `patch_evidence`: evidence of a fix once held has since been withdrawn, and
|
|
84
|
+
`remediation.fix_status` in the CVE's record is the current reading, not `patch_available` (since
|
|
85
|
+
3.0.2 the field's outputSchema text says so). An answer with no `patch_evidence` (an API older than that
|
|
86
|
+
field) does not name the evidence, and a `false` there is not a finding that no fix exists either.
|
|
87
|
+
Both tools relay the two fields as the API sends them.
|
|
88
|
+
|
|
89
|
+
## How `cve_exposure` counts
|
|
90
|
+
|
|
91
|
+
Every 12 hours, when Shodan query credits allow, the KEV-exposure radar runs one Shodan query
|
|
92
|
+
per tracked product, reads up to 100 ip:port services per query, and keeps a service when its
|
|
93
|
+
banner version matches a CISA-KEV or high-EPSS (>= 0.5) CVE. A service whose row has not been
|
|
94
|
+
written or refreshed for 21 days is dropped. A count is therefore a banner-version inference
|
|
95
|
+
over a sample: not an exploit test, and not an internet-wide census.
|
|
96
|
+
|
|
97
|
+
`last_seen` is when EchelonGraph last wrote or refreshed a service's row, not when the service
|
|
98
|
+
was observed, and not when its vulnerable version was last confirmed. It is set to the time of
|
|
99
|
+
the write when a search matches the banner, and again when a re-check finds the port still
|
|
100
|
+
listed by Shodan InternetDB, without re-reading the banner, so a patched service can stay
|
|
101
|
+
counted while its port stays open. Shodan's own time for the banner is not stored. So
|
|
102
|
+
`last_seen` dates the write, never the sighting, and it is never `measured_at` (see
|
|
103
|
+
[Structured results](../README.md#structured-results)).
|
|
104
|
+
|
|
105
|
+
The unit is a service, not a machine. Shodan returns one banner per port and the radar keys
|
|
106
|
+
each observation on ip:port, so a machine answering on two ports counts twice. The API's field
|
|
107
|
+
names are kept (`exposed_hosts` here; `distinct_hosts`, `ransomware_hosts` and each ranked
|
|
108
|
+
row's count in `exposure_radar`), but every one of them counts ip:port services.
|
|
109
|
+
|
|
110
|
+
The radar only looks for its **tracked** set of CVEs, so a zero means different things. The
|
|
111
|
+
note tags each case with its `exposure_state`, as "(exposure_state: …)"; that tag says what the
|
|
112
|
+
count is, and is not the result's `state`:
|
|
113
|
+
|
|
114
|
+
| API answer | What the tool says |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `tracked: true`, `exposed_hosts` above 0 | `exposure_state` `exposed`: the count of services the radar has on record for the CVE. |
|
|
117
|
+
| `tracked: false` | `exposure_state` `not_assessed`, **NOT ASSESSED**: outside the radar's tracked set; 0 is not a measurement. |
|
|
118
|
+
| `tracked: true`, `exposed_hosts: 0` | `exposure_state` `measured_zero`, a zero in the radar's sample: none of the up to 100 services it read per tracked-product query matched. Not an internet-wide zero, and undated: the answer does not say when the radar looked. The backend does not send this answer today. |
|
|
119
|
+
| no `tracked` field | The API did not say whether the CVE is in the tracked set: an older API, or the radar cannot decide (for example a CISA-KEV or high-EPSS CVE in a tracked product with 0 services on record). 0 exposed services on record, with no claim either way (`exposure_state` `tracking_unknown`). |
|
|
120
|
+
| HTTP 400, or an id that is not `CVE-YYYY-NNNN…` | An error result tagged `invalid_input`; nothing was looked up. |
|
|
121
|
+
|
|
122
|
+
Whatever the `exposure_state`, the result's `state` is `not_assessed` with `measured_at` `null`:
|
|
123
|
+
the answer does not say when any service it counts was observed (see [Structured results](../README.md#structured-results)).
|
|
124
|
+
|
|
125
|
+
## How `check_affected` decides
|
|
126
|
+
|
|
127
|
+
`check_affected` asks the matcher behind
|
|
128
|
+
[echelongraph.io/am-i-affected](https://echelongraph.io/am-i-affected) whether a version is
|
|
129
|
+
affected, by one of two lookup paths:
|
|
130
|
+
|
|
131
|
+
- **CPE path** — `product` and `version`. `product` is the NVD CPE product token (`openssl`,
|
|
132
|
+
`nginx`), which can differ from a package name. It returns the CVEs whose NVD CPE match
|
|
133
|
+
criteria name that product with a version range that includes the version. It matches the
|
|
134
|
+
token across vendors, so each match carries `cpe_vendor` and `vendor_unknown`: a match with
|
|
135
|
+
`vendor_unknown` true is real, but its vendor is not verified to be yours.
|
|
136
|
+
- **Registry path** — `ecosystem` (`npm`, `PyPI`, `Maven`, …), `package` and `version`. Each OSV
|
|
137
|
+
advisory record EchelonGraph holds for the package is decided against the version as
|
|
138
|
+
affected (a match), not affected (`not_affected_count`) or undetermined.
|
|
139
|
+
|
|
140
|
+
Read `assessed` before `count`. `assessed` false means the lookup did not evaluate the
|
|
141
|
+
component, and `not_assessed_reason` says why: `product_not_in_cpe_corpus`,
|
|
142
|
+
`package_not_cpe_nameable`, `candidate_load_pending` (with `degraded` true),
|
|
143
|
+
`candidate_window_truncated`, `package_not_in_advisory_corpus` or `no_decidable_advisory`. The
|
|
144
|
+
result's `state` is then `not_assessed`, and its `count` of 0 never means "not affected". An
|
|
145
|
+
advisory whose version range cannot be decided at this version is counted in
|
|
146
|
+
`undetermined_count` (up to 50 listed in `undetermined`) and is never reported as safe. `capped`
|
|
147
|
+
says the match list stopped at its cap, and `candidates_capped` that not every candidate CVE was
|
|
148
|
+
loaded. A failed advisory lookup (`advisory_lookup_failed`) comes back as a failure. Each match
|
|
149
|
+
keeps `kev_listed`, `ransomware`, `epss_score`, `effective_score`, `effective_severity` and
|
|
150
|
+
`score_assessed` as the API sent them.
|
|
151
|
+
|
|
152
|
+
A registry match also carries `interval`, the advisory interval that holds the version
|
|
153
|
+
(`introduced`, and `fixed`, exclusive, or `last_affected`, inclusive), and `fixed_in`, the lowest
|
|
154
|
+
version above the one asked about that no affected interval of the same record holds: the
|
|
155
|
+
holding interval's fixed bound, moved past any overlapping interval that still holds that bound,
|
|
156
|
+
and `fixed_in_detail` then names the intervals it moved past (#3074). `fixed_in` is always above
|
|
157
|
+
the version asked about. It is null where no such version is recorded (the walk reaches an
|
|
158
|
+
interval that ends at `last_affected`, or has no end, with none beyond it) or no interval holds
|
|
159
|
+
the version, and `fixed_in_reason` then says which: a null is not a finding that no fix exists. A CPE
|
|
160
|
+
match carries no `fixed_in`, since the end of a CPE version range is not a fix. `check_sbom` and
|
|
161
|
+
`scan_manifest` relay the same fields (`interval`, `fixed_in`, `fixed_in_reason` and
|
|
162
|
+
`fixed_in_detail`) on each match.
|
|
163
|
+
|
|
164
|
+
`product`, `version`, `ecosystem` and `package` travel in the `X-EG-Product`, `X-EG-Version`,
|
|
165
|
+
`X-EG-Ecosystem` and `X-EG-Package` request headers, never in the URL, so request logs and
|
|
166
|
+
trace spans that record URLs do not hold what was looked up. The endpoint reads a CPE vendor
|
|
167
|
+
from the URL only, so this tool takes none.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "echelongraph-mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"mcpName": "io.echelongraph/echelongraph-mcp",
|
|
5
5
|
"description": "MCP server for CVE lookups: EchelonGraph scores, CISA KEV, EPSS, CWE, exploits, version and SBOM checks, vendor advisories, and per-CVE exposure derived from Shodan data. Shodan data is owned by Shodan, which holds its copyright (© Shodan). Keyless.",
|
|
6
6
|
"type": "module",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
|
+
"docs",
|
|
12
13
|
"README.md",
|
|
13
14
|
"CHANGELOG.md",
|
|
14
15
|
"server.json"
|
package/server.json
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
]
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
|
-
"version": "
|
|
20
|
+
"version": "3.0.2",
|
|
21
21
|
"remotes": [
|
|
22
22
|
{
|
|
23
23
|
"type": "streamable-http",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
{
|
|
29
29
|
"registryType": "npm",
|
|
30
30
|
"identifier": "echelongraph-mcp",
|
|
31
|
-
"version": "
|
|
31
|
+
"version": "3.0.2",
|
|
32
32
|
"transport": {
|
|
33
33
|
"type": "stdio"
|
|
34
34
|
},
|