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/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": "2.7.2",
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": "2.7.2",
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": "2.7.2",
31
+ "version": "3.0.2",
32
32
  "transport": {
33
33
  "type": "stdio"
34
34
  },