echelongraph-mcp 3.0.0 → 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 +95 -0
- package/README.md +23 -7
- package/dist/fixStatus.js +4 -2
- package/dist/httpPolicy.js +16 -1
- package/dist/index.js +66 -9
- package/dist/prompts.js +8 -2
- package/dist/requiredText.js +15 -0
- package/dist/textBudget.js +281 -27
- package/dist/tools/check_affected.js +4 -1
- package/dist/tools/check_sbom.js +114 -4
- package/dist/tools/cve_intel.js +49 -4
- package/dist/tools/cve_remediation.js +82 -8
- package/dist/tools/epss_history.js +4 -1
- package/dist/tools/kev_recent.js +4 -1
- package/dist/tools/match_batch.js +22 -6
- package/dist/tools/scan_manifest.js +15 -3
- package/dist/tools/vendor_advisories.js +13 -4
- package/docs/tools.md +10 -6
- package/package.json +1 -1
- package/server.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,101 @@ with a provenance attestation by the release workflow of
|
|
|
6
6
|
[github.com/echelongraph/echelongraph-mcp](https://github.com/echelongraph/echelongraph-mcp),
|
|
7
7
|
from the commit tagged `v<version>`.
|
|
8
8
|
|
|
9
|
+
## 3.0.2 — 2026-10-17
|
|
10
|
+
|
|
11
|
+
3.0.1 was tagged but never published to npm: its release run's memory test
|
|
12
|
+
(`test/http-memory.test.mjs`) failed. With two 2,000-purl documents and 64 other requests in
|
|
13
|
+
flight, the hosted endpoint needed the whole of its image's 128 MiB heap, and V8 aborted. 3.0.2 is
|
|
14
|
+
3.0.1's changes with that fixed (the first entry).
|
|
15
|
+
|
|
16
|
+
- **The hosted image caps V8's old space at 104 MiB, not 128** (#3132, Dockerfile.http). With the
|
|
17
|
+
schemas built once, the memory test's live set fits in 80 MiB, and at 128 V8 let the burst's
|
|
18
|
+
garbage stand until the container peaked at 256-258 MB of its 268 MB. At 104 the peak is
|
|
19
|
+
235-241 MB with about 24 MiB of heap to spare (3.0.0 at 128: 238-252 MB). The start-up line's
|
|
20
|
+
`heap_limit_mb` now reads 152 (104 plus the young generation), not 176.
|
|
21
|
+
|
|
22
|
+
- **The hosted endpoint builds each tool's output schema once, not once per request** (#3132).
|
|
23
|
+
It serves every request with a server of its own, and 3.0.1's closed copy of each success
|
|
24
|
+
branch, the check behind the open `outputSchema` (below), doubled the output schemas each one
|
|
25
|
+
built: 1,371 KB of heap a server, against 3.0.0's 1,168. The memory test's two SBOMs were still
|
|
26
|
+
at their first batches when V8 aborted, so it was not their cut that ran out. Each schema is now
|
|
27
|
+
built once per process, 611 KB a server, and the memory test passes with an 80 MiB heap, where
|
|
28
|
+
3.0.0 needed 112 and 3.0.1 128. check_sbom's and scan_manifest's structured cut also measures a
|
|
29
|
+
thinned row and lets it go, and thins again only the rows it keeps: on that test's 2,000-row
|
|
30
|
+
answer it allocated 108 MB in 3.0.1, 18 MB in 3.0.0 and 24 MB now. No answer changes: every
|
|
31
|
+
call of the suite returns the same text and `structuredContent` as 3.0.1.
|
|
32
|
+
- **check_sbom and scan_manifest keep every row before any row leaves `data`** (#3132). In 3.0.0
|
|
33
|
+
the structured cut had one level for a row, which kept every match, so a row still large once
|
|
34
|
+
thinned left `data`, and every row after it went with it. Hosted 3.0.0 kept 1 of 5 rows for five
|
|
35
|
+
Debian 12 purls, all affected (linux-libc-dev 6.1.55-1 has 200 matches). It kept 2 of 51 rows for
|
|
36
|
+
Juice Shop 11.1.2 with `pkg:composer/magento/community-edition@2.3.0` at index 2. For that purl
|
|
37
|
+
alone it kept no row, and the note said "check 0 or fewer purls per call". Now, past the thinned
|
|
38
|
+
level, every row's matches are capped at the same number, the most that fit. A row with more
|
|
39
|
+
keeps its first matches (highest `effective_score` first) and carries `matches_truncated`: its
|
|
40
|
+
total, the number returned, and the KEV-listed and ransomware CVE IDs and the `fixed_in` versions
|
|
41
|
+
of all its matches. Next, each match is cut to `cve_id`, `kev_listed`, `ransomware`, `fixed_in`
|
|
42
|
+
and `effective_score`, the row keeping `matched_package`, `matched_via` and an upstream row's
|
|
43
|
+
`matched_version_from` and `purl_name`. Only when that cannot keep every row is each row cut to
|
|
44
|
+
`index`, `purl`, `verdict`, `not_assessed_reason` and `cve_ids`. Rows leave only when even that
|
|
45
|
+
cannot keep them all, clean rows first as before, and they leave at the compact match level, at
|
|
46
|
+
most 10 matches a row, so each row kept still says its KEV flags and `fixed_in`. Once the cut is
|
|
47
|
+
settled, the rows kept take back detail in input order while room is left. Replayed on those
|
|
48
|
+
answers, the three cases keep 5 of 5 rows, 51 of 51 and 1 of 1. On the production synthetic's
|
|
49
|
+
201-purl probe, all affected, 3.0.0 kept 43 rows; this release keeps 54 compact ones, the
|
|
50
|
+
log4j-core 2.14.1 row with CVE-2021-44228 (`fixed_in` 2.15.0). The note no longer says
|
|
51
|
+
"0 or fewer".
|
|
52
|
+
- **`not_sent_purls` is kept whole or left out, never cut short** (#3132). It is the list a client
|
|
53
|
+
sends again. When a `DATA CUT` leaves it out, check_sbom's note gives the purls not sent as
|
|
54
|
+
positions in the input: `coverage.sent` + 1 to `coverage.distinct_purls`, in the order the note
|
|
55
|
+
states. scan_manifest's positions would count purls read from the files, which a client cannot
|
|
56
|
+
rebuild, so its note says the same call with `full: true` returns the list instead.
|
|
57
|
+
- **cve_remediation drops a vendor advisory only after `patches` and `fixed_branches`** (#3139).
|
|
58
|
+
CVE-2023-44487's default answer kept 2 of its 20 advisories, while `fixed_branches` (50 rows) and
|
|
59
|
+
`patches` (30) stayed whole. Now every advisory is kept, thinned. Then `fixed_branches` is thinned
|
|
60
|
+
(to the package, its range, `fixed_version` and each range's `introduced`, `fixed` and
|
|
61
|
+
`last_affected`; each range's `source` and `advisory_id` go), and each package's ranges are
|
|
62
|
+
capped at the most that fit, a package so cut carrying `fixed_branches_truncated` (total and
|
|
63
|
+
returned). Then `patches` are thinned (to `vendor`, `patch_url` and `published_at`), every one
|
|
64
|
+
kept, and `fix_references` is cut. Only then do `patches` rows leave, then `fixed_branches` rows.
|
|
65
|
+
Room left goes back to the capped ranges. Replayed, that answer keeps all 20 advisories, all 50
|
|
66
|
+
`fixed_branches` and all 30 `patches`, each thinned, and 17 of 20 `fix_references`, at 29,931
|
|
67
|
+
characters. When
|
|
68
|
+
advisories do leave, the `DATA CUT` note names `vendor_advisories_for_cve`, which lists every
|
|
69
|
+
one.
|
|
70
|
+
- **`patch_available`'s schema text says what `true` beside an empty `patch_evidence` means**
|
|
71
|
+
(#3138). It means evidence of a fix once held has since been withdrawn: the flag is never set
|
|
72
|
+
back to false. `remediation.fix_status` is then the current reading. The text used to say `true`
|
|
73
|
+
means EchelonGraph holds evidence of a fix, while hosted get_cve CVE-2026-1281 answered `true`
|
|
74
|
+
beside `[]` and `none_confirmed`. get_cve's and search_cves' descriptions now say `true` means
|
|
75
|
+
EchelonGraph holds, or once held, fix evidence for the CVE, and `patch_evidence` is what it holds
|
|
76
|
+
now.
|
|
77
|
+
- **A success's `outputSchema` admits top-level fields it does not name** (#3121). A client
|
|
78
|
+
validates answers against the tool list it read, and the hosted endpoint never tells it to read
|
|
79
|
+
again. In 3.0.0 a Claude Code session that listed the tools under 2.7.2 failed every tool's
|
|
80
|
+
call: "data must NOT have additional properties", from `data_truncated`, which 3.0.0 added to
|
|
81
|
+
every success. A field added at the top level in a minor release no longer breaks such a client.
|
|
82
|
+
The failure branch and each tool's `data` schema stay closed, so a field added inside `data`
|
|
83
|
+
still needs a reconnect, and the server still checks its own answers against the closed success
|
|
84
|
+
branch (`test/checked-strict.test.mjs`). If you see "does not match the tool's output schema" after an upgrade, reconnect
|
|
85
|
+
the client (README, Versioning).
|
|
86
|
+
- `test/text-bound-live.mjs --hosted` calls a 3.0.0 or later endpoint with `full: true`. It
|
|
87
|
+
refuses to replay a cut `data` as the API's answer (#3140). Without `full: true`, get_cve
|
|
88
|
+
CVE-2021-44228 replayed 77 of its 396 `cpe_match` entries. The vendor tools' `/coverage` is now
|
|
89
|
+
served from the case's fixture beside the replayed answer, not as that answer. The record
|
|
90
|
+
(`test/fixtures/text-bound-live.json`) was re-measured on hosted 3.0.0 and holds a structured size
|
|
91
|
+
for each of the 23 cases measured; scan_manifest's 64-package lock, cve_remediation and cve_intel
|
|
92
|
+
with `fixed_branches` are measured on production for the first time. Four fixtures were re-read
|
|
93
|
+
where production had moved more than 10% from them (search_cves, epss_history,
|
|
94
|
+
vendor_advisories_for_cve, and scan_manifest's lock, which now replays production's own rows).
|
|
95
|
+
|
|
96
|
+
- **The hosted endpoint's `mcp_handler_error` log names the cause of createMcpHandler's errors**
|
|
97
|
+
(#2923). 2.7.1's `error_class` matched only the legacy transport's messages, so all 78 production
|
|
98
|
+
lines read from 2026-10-06 were `other`. It now also matches the bundled SDK's "Rejected inbound
|
|
99
|
+
request (", "Rejected 2025-era request on a modern-only endpoint (", "Unsupported protocol
|
|
100
|
+
version" (code -32022, and the legacy transport's "Bad Request: " form) and "Missing required
|
|
101
|
+
client capabilities". As before, only the label and the numeric code are logged, never the
|
|
102
|
+
message.
|
|
103
|
+
|
|
9
104
|
## 3.0.0 — 2026-10-11
|
|
10
105
|
|
|
11
106
|
A major version (README, Versioning): the structured result is now held to 30,000 characters by
|
package/README.md
CHANGED
|
@@ -255,7 +255,7 @@ The example questions are ones a client can answer with that tool alone.
|
|
|
255
255
|
| `check_affected` | Am I affected? (product or package at a version) | Whether a product (its NVD CPE product token) or a registry package (`ecosystem` and `package`) at a given version is affected by known CVEs, from the matcher behind echelongraph.io/am-i-affected: `assessed` first (false: not evaluated, with `not_assessed_reason`, and a count of 0 then is not "not affected"), the matching CVEs with `kev_listed`, `ransomware`, `epss_score`, `effective_score` and `score_assessed` (a registry match also `fixed_in`, the lowest version above it that no affected interval of the record holds, with `fixed_in_detail` when it moved past an overlapping interval, or null with `fixed_in_reason`, which is not a finding that no fix exists), and advisories it cannot decide counted as `undetermined_count`, never as safe. What you look up travels in request headers, never in the URL. | *Is openssl 3.0.0 affected by known CVEs? Is lodash 4.17.15 on npm?* |
|
|
256
256
|
| `check_sbom` | Check an SBOM against the advisory corpus | A dependency list checked against EchelonGraph's advisory corpus (OSV.dev records), one verdict per component: `affected`, `not_affected`, `undetermined` or `not_assessed`, each with its `not_assessed_reason`. Each row names the package its advisories were looked up by (`matched_package`) and how (`matched_via`: `upstream` where a deb, apk or alpine purl's upstream qualifier names the source package, which is how Debian and Alpine file advisories, else `purl_name`), and each match carries `fixed_in`, the lowest version above the component's that no affected interval of the record holds (`fixed_in_detail` names any overlapping interval it moved past), or null with `fixed_in_reason`. Pass up to 2,000 distinct purls, or a CycloneDX JSON or SPDX JSON document: the purls are read from it by the MCP server and only they are sent to the API, in POST bodies of at most 200 each, one after another; the document itself is not sent on. Run from npm, the server is on your machine; over the hosted endpoint (`mcp.echelongraph.io`) it is EchelonGraph's, and the document is the request body. When the API's per-component rate limit answers 429, the tool waits the `Retry-After` it names, within 50 seconds per call; past that it answers what it has, with the purls not sent counted in `coverage.not_sent`, the reason in `coverage.not_sent_reason`, and the list in `data.not_sent_purls` (whole with `full: true`; a `DATA CUT` can cut it), never dropped silently. A deb, apk or rpm purl without a `distro` qualifier naming its release is not assessed (`distro_release_unknown`): EchelonGraph does not guess a release. Only `not_affected` is clean. No ranking, no score. | *Check this CycloneDX SBOM against the advisory corpus.* |
|
|
257
257
|
| `scan_manifest` | Check a lockfile against the advisory corpus | A project's lockfiles or pinned manifests checked against the same advisory corpus, one verdict per dependency, through the same batch route, batches, 50-second budget and `Retry-After` waits as `check_sbom`. Pass `files`, 1 to 20 of `filename` and `content` (the file's text), up to 5,000,000 characters in all; the format is read from the filename, or set by `format`. Read: `requirements.txt` (only `==` and `===` pins; extras and markers stripped), `go.mod` (`require`, single-line and block, `// indirect` included, with `replace` and `exclude` applied), `package-lock.json` and `npm-shrinkwrap.json` (lockfileVersion 1 to 3), `Cargo.lock`, `Gemfile.lock` (platform suffixes stripped), `composer.lock` (a leading `v` stripped), `poetry.lock` and `gradle.lockfile`. `package.json`, `pyproject.toml`, `Pipfile`, `Gemfile`, `Cargo.toml`, `composer.json` and `build.gradle` hold ranges and are refused, each naming the lockfile to pass; `go.sum` is refused, as it lists module versions the build does not select; `pom.xml`, `yarn.lock` and `pnpm-lock.yaml` are not read yet. The MCP server reads the files into purls and only the purls are sent to the API; filenames and file contents are not sent on. Run from npm, the server is on your machine; over the hosted endpoint (`mcp.echelongraph.io`) it is EchelonGraph's, and the files are the request body. `data.results` holds one row per distinct purl, as `check_sbom`'s do, each match with `kev_listed`, `epss_score` and `fixed_in`. Every entry not sent is in `data.not_checked`, with its file, line and `reason`: `version_unpinned` (a range, such as `django>=4`, whose bound is never sent as a version), `version_unresolved`, `local_path`, `vcs_source` or `unsupported_line`, counted in `coverage.not_checked_by_reason`; they are not clean. A file that holds no version this tool can check answers `not_assessed` without calling the API. Only `not_affected` is clean. No ranking, no score. | *Check this package-lock.json against the advisory corpus.* |
|
|
258
|
-
| `cve_intel` | CVE weakness, exploits and packages | Weakness,
|
|
258
|
+
| `cve_intel` | CVE weakness, exploits and packages | Weakness, exploit code and detection templates (a Nuclei template checks whether a system is affected; it is not necessarily exploit code, and the note counts templates apart), affected packages and fixed versions for one CVE, from EchelonGraph's per-CVE enrichment: `cwes`, `exploits` (at most 10, verified first) with `exploits_total`, `exploits_capped`, `exploits_by_kind` and `exploits_by_status`, `affected_packages`, `fixed_versions` and `timeline`. Each `affected_packages` row carries `fixed_branches`, every affected range of the package with its fix (`introduced`, and `fixed` or, where no fix is on record, `last_affected`), each naming the OSV record that published it (`advisory_id`, `source`) and in that record's order: an installed version's fix is the first `fixed` above it that no range of its row holds (the `fixed` of the range that holds it, moved past any range that still holds that version; none past a range without `fixed`), compared in the ecosystem's version order. `fixed_version` is one range's fix, kept for compatibility, not the fix for every range (CVE-2021-44228's log4j-core: 2.12.2, the fix for 2.4 to 2.12.1, while 2.14.1's range is fixed in 2.15.0); `fixed_branches` is null where a package's ranges were never loaded, and `[]` where no version range is on record, which is not a finding that no fix exists. `verified_status` is the label stored with each reference, not a guarantee that the exploit works. An empty `exploits` list is not evidence that no public exploit exists; a section the API could not read is named in `coverage.sections_failed`, never relayed as an empty list. | *Is there public exploit code for CVE-2021-44228, and which versions fix it?* |
|
|
259
259
|
| `cve_remediation` | How one CVE is fixed | How one CVE is fixed, as its sources state it, in one answer; every text is relayed as its source states it, untested by EchelonGraph: `cisa` (for a KEV-listed CVE, CISA's `required_action`, `due_date`, `short_description` and `notes_urls`, verbatim; CISA directs the action at US federal civilian agencies, and it is not EchelonGraph's advice), `fixed_branches` (each affected package with every affected range and its fix), `vendor_remediations` (each vendor advisory naming the CVE, at most 20, with its `remediation_state` and the items it lists for the CVE, each with `kind`, the vendor's own category: `vendor_fix`, `workaround`, `mitigation`, `no_fix_planned`, `none_available` or `other`), `fix_references` (references NVD or the CNA tagged Patch or Mitigation), `patches`, `coverage` and `failed_sections`, with `fix_status`, the API's fix-status rule that CVE Pulse pages show. Given the installed `ecosystem`, `package` and `version` (all three or none), it also answers `installed` (that version's verdict) and `fixed_in`: the first version above it that no affected range on record holds, or null with `fixed_in_reason`. `remediation_state` `none_in_source` or `not_parsed`, and an empty list, are not a finding that no fix exists. A REJECTED CVE answers `not_assessed`. | *How is CVE-2021-44228 fixed, as its sources state it?* |
|
|
260
260
|
| `get_cwe` | CWE and its CVEs | One CWE (weakness class) and the CVEs classified under it: `name` and `description` from the MITRE CWE catalog EchelonGraph embeds, `total`, and one page of 50 `cves`, ordered as `order` states (CISA-KEV-listed first, then EchelonGraph score). A `total` of 0 says that no CVE in EchelonGraph's feed is classified under that CWE, not that none exists. | *Which CVEs are classified under CWE-79, CISA-KEV-listed first?* |
|
|
261
261
|
| `vendor_advisories_for_cve` | Vendor advisories for one CVE | The vendor-published advisories (Microsoft MSRC, Red Hat, Cisco, Palo Alto Networks, GitHub GHSA and the other feeds EchelonGraph polls) that name one CVE, newest first, at most 20, each with `remediation_kinds` and `remediation_state` (`not_parsed`: EchelonGraph does not read that vendor's remediation, which is not a finding of none). | *Which vendor advisories name CVE-2024-3400?* |
|
|
@@ -396,13 +396,22 @@ tool states. Where one row is large, every row is first thinned, all rows kept:
|
|
|
396
396
|
`scan_manifest` keep each row's `index`, `purl`, `verdict`, `not_assessed_reason`, `cve_ids`,
|
|
397
397
|
`matched_package` and `matched_via`, and per match `cve_id`, `kev_listed`, `ransomware`, `epss_score`,
|
|
398
398
|
`effective_score`, `score_assessed`, `fixed_in` and `fixed_in_reason`, every field a prompt reads from
|
|
399
|
-
a match
|
|
399
|
+
a match, then each row's matches to at most the same
|
|
400
|
+
number, its first, a row so cut carrying `matches_truncated` (its total, and the KEV-listed and
|
|
401
|
+
ransomware CVE IDs and `fixed_in` versions of all its matches), then each match to `cve_id`,
|
|
402
|
+
`kev_listed`, `ransomware`, `fixed_in` and `effective_score`, then each row to `index`, `purl`,
|
|
403
|
+
`verdict`, `not_assessed_reason` and `cve_ids` (rows leave at the compact level; the rows kept
|
|
404
|
+
take back detail in order); `check_affected` keeps each
|
|
400
405
|
match's scores, flags, criteria and `fixed_in`, then fewer; `cve_remediation` drops each item's
|
|
401
|
-
`product_ids`, then its text, keeping every advisory
|
|
402
|
-
|
|
406
|
+
`product_ids`, then its text, keeping every advisory's state, kinds, URLs and fixed builds
|
|
407
|
+
(`get_vendor_advisory` returns each text), thins `fixed_branches` (then caps each
|
|
408
|
+
package's ranges) and `patches`, then cuts `fix_references`, then `patches`, then
|
|
409
|
+
`fixed_branches`, advisories last, the note naming `vendor_advisories_for_cve`, which
|
|
410
|
+
lists them all; `search_cves` keeps each row's
|
|
403
411
|
description and scores, then the scores alone; the vendor-advisory
|
|
404
412
|
tools drop `affected_products`, then `summary`. Only then do rows leave: `check_sbom` and
|
|
405
|
-
`scan_manifest` cut `
|
|
413
|
+
`scan_manifest` cut `not_checked`, then `not_sent_purls`, whole or none (the note says how to
|
|
414
|
+
get it), then drop the `not_affected` rows,
|
|
406
415
|
then the `not_assessed` ones (`data.summary` counts every verdict), and only then affected rows,
|
|
407
416
|
from the end; `check_affected` drops its `excluded` and `undetermined` samples first and its
|
|
408
417
|
`cve_ids`, which names every match, last; the other row tools drop rows from the end, and those that
|
|
@@ -413,7 +422,8 @@ also the page size whose rows fit `data` whole, counted on the rows whole). `get
|
|
|
413
422
|
entries, then every ranking, then `kev_exposure.newest_kev`; then any other list at the top of
|
|
414
423
|
`data` is cut the same way. The room left once a list fits goes back to the lists emptied before
|
|
415
424
|
it, as many of their entries as fit (their first, a series its newest). No string is shortened and no value changes (a thinned row
|
|
416
|
-
keeps some of its fields, each optional in the outputSchema, so `data` still validates
|
|
425
|
+
keeps some of its fields, each optional in the outputSchema, so `data` still validates;
|
|
426
|
+
a cut adds only `matches_truncated` and `fixed_branches_truncated`). The result
|
|
417
427
|
then says `data_truncated: true` (`false` on every other success), `truncated` gives each list cut,
|
|
418
428
|
by its path in `data`, with its `total`, the number `returned` and, where its entries are thinned,
|
|
419
429
|
`thinned: true`, and the note ends with sentences starting `DATA CUT` that say what was cut, how to
|
|
@@ -611,7 +621,7 @@ The npm package (stdio):
|
|
|
611
621
|
The HTTP entrypoint, `dist/http.js` (`npm run start:http`), serves the same tools over
|
|
612
622
|
Streamable HTTP on `POST /mcp`, with a liveness check on `GET /health`. In a container with
|
|
613
623
|
little memory, start it with V8's heap capped below the container's limit, as EchelonGraph's
|
|
614
|
-
hosted deployment does
|
|
624
|
+
hosted deployment does in 256 MiB (`node --max-old-space-size=104 --max-semi-space-size=16 dist/http.js`): Node
|
|
615
625
|
otherwise sizes its heap from the memory the system reports, which in a container can be the
|
|
616
626
|
host's, and the container is then killed before V8 collects.
|
|
617
627
|
It reads the three variables above and:
|
|
@@ -657,6 +667,12 @@ shape. Every release is published to npm with a provenance attestation by the pu
|
|
|
657
667
|
release workflow, from the commit tagged `v<version>`. Changes per version:
|
|
658
668
|
[CHANGELOG.md](CHANGELOG.md).
|
|
659
669
|
|
|
670
|
+
Since 3.0.2 a success's `outputSchema` admits top-level fields it does not name, so a field added
|
|
671
|
+
in a minor release does not fail a client still holding the tool list it read before. A client
|
|
672
|
+
that shows "does not match the tool's output schema" after an upgrade is validating against the
|
|
673
|
+
schema of the version it listed: reconnect it (Claude Code: `/mcp`, then reconnect `echelongraph`) or restart it. Each tool's `data`
|
|
674
|
+
schema stays closed: a field added inside it needs that reconnect too.
|
|
675
|
+
|
|
660
676
|
## Security
|
|
661
677
|
|
|
662
678
|
Report a vulnerability privately to **support@echelongraph.io**, not in a public issue. The
|
package/dist/fixStatus.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// first.
|
|
8
8
|
import * as z from "zod";
|
|
9
9
|
export const FIX_STATES = ["available", "fix_referenced", "fix_merged", "none_confirmed", "unknown"];
|
|
10
|
-
export const FIX_STATUS_FIELD = "The API's fix-status rule for the CVE, the one CVE Pulse pages show: state is available (a fix is on record), fix_referenced (a reference tagged Patch, often a source commit, and no release confirmed), fix_merged (an upstream fix merged, not yet in a tagged release), none_confirmed (no fix evidence on record, which is not a finding that no fix exists) or unknown (an input could not be read). basis is the step of the rule that decided it; described_by is the CVE description's sentence naming the fix, solution_by the CVE record's solution sentence naming it (statement) and the CNA that wrote it (cna), stated_in_record that patch_evidence counts a fix the CVE record states with no sentence to quote, listed_in_patch_table that a fixed version or patch row is on record; unread_inputs names any input the API could not read.";
|
|
10
|
+
export const FIX_STATUS_FIELD = "The API's fix-status rule for the CVE, the one CVE Pulse pages show: state is available (a fix is on record), fix_referenced (a reference tagged Patch, often a source commit, and no release confirmed), fix_merged (an upstream fix merged, not yet in a tagged release), none_confirmed (no fix evidence on record, which is not a finding that no fix exists) or unknown (an input could not be read). basis is the step of the rule that decided it; described_by is the CVE description's sentence naming the fix, solution_by the CVE record's solution sentence naming it (statement) and the CNA that wrote it (cna), stated_in_record that patch_evidence counts a fix the CVE record states with no sentence to quote, listed_in_patch_table that a fixed version or patch row is on record; unread_inputs names any input the API could not read. vendor_released (basis vendor_fix_released) is the vendor's own statement that a fix is released where the CVE record names no fixed version: its sentence, verbatim (statement, or null), never a version, so the fixed release is the vendor advisory's to name. solution_by's section is workarounds when the fix sentence is the record's workaround, not its solution.";
|
|
11
11
|
const opt = (t) => t.nullable().optional();
|
|
12
12
|
export function fixStatusSchema() {
|
|
13
13
|
return z
|
|
@@ -17,9 +17,11 @@ export function fixStatusSchema() {
|
|
|
17
17
|
basis: opt(z.string()),
|
|
18
18
|
described_by: opt(z.string()),
|
|
19
19
|
// #3076 (merged from 24dad5eb): the CVE record's own fix sentence, and its CNA.
|
|
20
|
-
solution_by: opt(z.looseObject({ statement: opt(z.string()), cna: opt(z.string()) })),
|
|
20
|
+
solution_by: opt(z.looseObject({ statement: opt(z.string()), cna: opt(z.string()), section: opt(z.string()) })),
|
|
21
21
|
stated_in_record: opt(z.boolean()),
|
|
22
22
|
listed_in_patch_table: opt(z.boolean()),
|
|
23
|
+
// #3076, third pass: the vendor's released-fix statement with no version (cause G).
|
|
24
|
+
vendor_released: opt(z.looseObject({ statement: opt(z.string()) })),
|
|
23
25
|
unread_inputs: opt(z.array(z.string())),
|
|
24
26
|
})
|
|
25
27
|
.nullable()
|
package/dist/httpPolicy.js
CHANGED
|
@@ -295,7 +295,7 @@ export class Slots {
|
|
|
295
295
|
// - V8's heap. In that sandbox (a cgroup v1 on a 16 GB host) Node 22 sized V8's heap from the
|
|
296
296
|
// host's memory, not the cgroup's (heap_size_limit 8,195 MiB in 256 MiB), so V8 left the
|
|
297
297
|
// garbage of the requests served (~1 MB each) uncollected until the kernel killed the
|
|
298
|
-
// instance. So the image caps V8's old space at
|
|
298
|
+
// instance. So the image caps V8's old space at 104 MiB (Dockerfile.http; 128 until 3.0.2), and V8 collects
|
|
299
299
|
// first. On Cloud Run this is not verified: an instance that shows Node only its 256 MiB gets
|
|
300
300
|
// a default heap near the cap already, and then the cap changes little. Its start-up line says
|
|
301
301
|
// which (heap_limit_mb; memory_limit_mb, null where Node is told no limit).
|
|
@@ -482,7 +482,22 @@ export function uaFamily(ua) {
|
|
|
482
482
|
// method name), so the message is never logged. It is matched against the SDK's own fixed
|
|
483
483
|
// prefixes and only the matching label is logged; anything unrecognised is "other". A
|
|
484
484
|
// ProtocolError also carries its numeric JSON-RPC code, which is content-free.
|
|
485
|
+
//
|
|
486
|
+
// #2923 (2.7.1 audit, 2026-10-10): 78 of 78 production lines were "other": the list held the legacy
|
|
487
|
+
// transport's messages, not those of the bundled SDK's createMcpHandler (@modelcontextprotocol/server
|
|
488
|
+
// 2.1.0 dist/index.mjs), which reports "Rejected inbound request (<cell>): …" (:1346, :1384, :1458),
|
|
489
|
+
// "Rejected 2025-era request on a modern-only endpoint (<cell>): …" (:1438), an
|
|
490
|
+
// UnsupportedProtocolVersionError, "Unsupported protocol version: <requested>", code -32022 (:1338;
|
|
491
|
+
// src-D-y6h4N7.mjs:3693), and a MissingRequiredClientCapabilityError, "Missing required client
|
|
492
|
+
// capabilities: …" (:1357; src-D-y6h4N7.mjs:3727). The legacy transport's own wording is
|
|
493
|
+
// "Bad Request: Unsupported protocol version: …" (index.mjs:896). All of these lead the list, each
|
|
494
|
+
// with its fixed prefix only (the cell and the rest can name the client's method or version).
|
|
485
495
|
const HANDLER_ERROR_CLASSES = [
|
|
496
|
+
["Rejected 2025-era request on a modern-only endpoint (", "rejected_legacy_on_modern_only"],
|
|
497
|
+
["Rejected inbound request (", "rejected_inbound_request"],
|
|
498
|
+
["Unsupported protocol version", "unsupported_protocol_version"],
|
|
499
|
+
["Bad Request: Unsupported protocol version", "unsupported_protocol_version"],
|
|
500
|
+
["Missing required client capabilities", "missing_client_capability"],
|
|
486
501
|
["Not Acceptable", "not_acceptable"],
|
|
487
502
|
["Unsupported Media Type", "unsupported_media_type"],
|
|
488
503
|
["Method not allowed", "method_not_allowed"],
|
package/dist/index.js
CHANGED
|
@@ -72,7 +72,7 @@ import { registerVendorAdvisoryTools } from "./tools/vendor_advisories.js";
|
|
|
72
72
|
import { registerPrompts } from "./prompts.js";
|
|
73
73
|
import { registerResources } from "./resources.js";
|
|
74
74
|
import { answering, answeringNow, cutData, dataText, FIRST_TEXT_BLOCK_BESIDE_A_CUT, FULL_ARG, GET_CVE_WHOLE, NO_DATA_CUT, STRUCTURED_BUDGET, TEXT_BUDGET_DESCRIPTION, wholePage, } from "./textBudget.js";
|
|
75
|
-
import { SHODAN_ATTRIBUTION, SHODAN_OWNERSHIP } from "./requiredText.js";
|
|
75
|
+
import { PATCH_AVAILABLE_WITHDRAWN, SHODAN_ATTRIBUTION, SHODAN_OWNERSHIP } from "./requiredText.js";
|
|
76
76
|
// The package actually running, read from the package.json that ships beside dist/. The MCP
|
|
77
77
|
// handshake (serverInfo) and the User-Agent both carry its name and version, so the API's
|
|
78
78
|
// access log can count adoption per version without a code change per release.
|
|
@@ -374,7 +374,9 @@ function unexpectedShape(tool, error) {
|
|
|
374
374
|
export const checked = (tool, schema, r) => {
|
|
375
375
|
if (r.isError)
|
|
376
376
|
return r;
|
|
377
|
-
|
|
377
|
+
// #3121: the published schema's success branch is open; this server's own answers are held to
|
|
378
|
+
// the closed one.
|
|
379
|
+
const p = (STRICT.get(schema) ?? schema).safeParse(r.structuredContent);
|
|
378
380
|
return p.success ? r : unexpectedShape(tool, p.error);
|
|
379
381
|
};
|
|
380
382
|
// A success: content[0] is the data (cut to DATA_TEXT_BUDGET when it is larger, #2783), content[1]
|
|
@@ -869,11 +871,17 @@ function cnaRecordNote(cve, rec) {
|
|
|
869
871
|
const by = who ? `its CNA, ${who},` : "its CNA";
|
|
870
872
|
const date = strAt(rec, "date_public");
|
|
871
873
|
const fix = strAt(rec, "cna_solution");
|
|
874
|
+
// #3178: a sentence of the record's workarounds is said to be its workaround.
|
|
875
|
+
const workaround = strAt(rec, "cna_solution_section") === "workaround";
|
|
872
876
|
const parts = [];
|
|
873
877
|
if (date)
|
|
874
878
|
parts.push(` date_public is when ${cve} was made public as ${by} states it in the CVE record (datePublic), not the record's publication date.`);
|
|
875
|
-
if (fix)
|
|
879
|
+
if (fix && workaround) {
|
|
880
|
+
parts.push(` cna_solution is a fix statement from the CVE record's workarounds (cna_solution_section workaround), not its solutions, written by ${by} and relayed verbatim: it names the fixed release, and is not EchelonGraph's advice.`);
|
|
881
|
+
}
|
|
882
|
+
else if (fix) {
|
|
876
883
|
parts.push(` cna_solution is the CVE record's own fix statement, written by ${by} and relayed verbatim: it names the fixed release, and is not EchelonGraph's advice.`);
|
|
884
|
+
}
|
|
877
885
|
return parts.join("");
|
|
878
886
|
}
|
|
879
887
|
function remediationSummaryNote(cve, rec) {
|
|
@@ -2226,10 +2234,43 @@ const SUCCESS_STATE = "measured: a measurement of what was asked; an exposure co
|
|
|
2226
2234
|
const DATA_TRUNCATED_FIELD = "false: data is the answer whole. true: the structured result would pass 30,000 characters of JSON with data whole, so data is cut (DATA CUT): the entries of some of its lists keep only some of their fields, or the lists only some of their entries, truncated gives each, and no string is shortened. The same call with full: true returns data whole.";
|
|
2227
2235
|
const TRUNCATED_FIELD = "Present when data_truncated is true: each list of data that was cut, by its path in data (a dot between the names of an object and a list inside it), with total, its entries in the answer, returned, the entries data holds, and thinned, true when each of them keeps only some of its fields.";
|
|
2228
2236
|
const TRUNCATED = z.record(z.string(), z.strictObject({ total: z.number().int().min(0), returned: z.number().int().min(0), thinned: z.literal(true).optional() }));
|
|
2237
|
+
// #3121: the success branch is published open to fields it does not name, and this server holds
|
|
2238
|
+
// its own answers to the closed one (STRICT, read by checked). Through 3.0.0 both branches were
|
|
2239
|
+
// closed (additionalProperties false), and a client validates against the outputSchema it listed,
|
|
2240
|
+
// which the hosted endpoint never tells it to list again (stateless, listChanged false): a Claude
|
|
2241
|
+
// Code session that listed the tools under 2.7.2 failed its first check_sbom call once 3.0.0 went
|
|
2242
|
+
// live, "Structured content does not match the tool's output schema: data must NOT have additional
|
|
2243
|
+
// properties" (ajv's root is "data"; 3.0.0 added data_truncated and truncated to every success),
|
|
2244
|
+
// until it reconnected. So a field added beside them in a minor release would break every such
|
|
2245
|
+
// session too, against the README's semver promise. Opening the success branch keeps such a field
|
|
2246
|
+
// additive for a client holding an older list; the failure branch stays closed (a failure is never
|
|
2247
|
+
// read as data), and data keeps each tool's own schema. The suite validates answers against the
|
|
2248
|
+
// closed branch (test/strict-output.mjs), and checked() does here, so a field this server adds
|
|
2249
|
+
// without naming it in the schema still fails, as it did when the branch was closed.
|
|
2250
|
+
//
|
|
2251
|
+
// 3.0.2: every output schema is built once per process, and its strict copy with it. The hosted
|
|
2252
|
+
// endpoint builds a server for each request (http.ts: createMcpHandler's stateless leg calls
|
|
2253
|
+
// createServer), and through 3.0.1 the tool modules that take envelopeSchema from this file
|
|
2254
|
+
// (check_affected, check_sbom, epss_history, kev_recent, scan_manifest and the three vendor-advisory
|
|
2255
|
+
// tools) built their output schemas in their register functions, so once per request in flight.
|
|
2256
|
+
// 3.0.1's strict copy doubled those: 1,371 KB of heap per server, against 3.0.0's 1,168
|
|
2257
|
+
// (64 servers held at once, node --expose-gc). test/http-memory.test.mjs's "served as they come"
|
|
2258
|
+
// case holds 64 requests in flight beside two 5,000,000-character SBOMs; it failed the 3.0.1
|
|
2259
|
+
// release run at Dockerfile.http's 128 MiB, and passed here only at 128, where 3.0.0 passes at
|
|
2260
|
+
// 112. At 112 V8 aborted 3.5 s in, with 59 servers live and both documents still at their first
|
|
2261
|
+
// batches, so before either answer was cut (a heap snapshot near the limit). Each such module now
|
|
2262
|
+
// keeps its schemas per envelopeSchema, as cve_intel, cve_remediation and get_cwe already did
|
|
2263
|
+
// (schemasOnce): 611 KB a server, and the case passes at --max-old-space-size=80.
|
|
2264
|
+
const STRICT = new WeakMap();
|
|
2229
2265
|
export function envelopeSchema(o) {
|
|
2266
|
+
const published = envelopeOf(o, z.looseObject);
|
|
2267
|
+
STRICT.set(published, envelopeOf(o, z.strictObject));
|
|
2268
|
+
return published;
|
|
2269
|
+
}
|
|
2270
|
+
function envelopeOf(o, success) {
|
|
2230
2271
|
const coverage = o.coverage ? o.coverage.nullable() : z.null();
|
|
2231
2272
|
return z.discriminatedUnion("state", [
|
|
2232
|
-
|
|
2273
|
+
success({
|
|
2233
2274
|
state: z.enum(["measured", "not_assessed"]).describe(SUCCESS_STATE),
|
|
2234
2275
|
measured_at: Instant.nullable().describe("When the underlying observation was made, as the API states it; null when the answer does not say or holds no observation."),
|
|
2235
2276
|
method: z.string().describe("How the numbers were produced."),
|
|
@@ -2275,8 +2316,8 @@ const CPE_CONFIGURATIONS_DESCRIPTION = "NVD's configurations array as NVD sent i
|
|
|
2275
2316
|
// templates, internal/cve/fix_statement.go). The last two are the record's own words: get_cve
|
|
2276
2317
|
// relays the solution's sentence as cna_solution, and the description is in the record.
|
|
2277
2318
|
// One constant string each (the site's tool-claims check folds only constant strings).
|
|
2278
|
-
const PATCH_EVIDENCE_LABELS = ["vendor_patch", "fixed_version", "release_reference", "vendor_advisory_fix", "nvd_patch_reference", "cna_patch_reference", "cna_solution", "description_fix"];
|
|
2279
|
-
const PATCH_EVIDENCE_SOURCES = "vendor_patch (a vendor patch on record, from Ubuntu or Red Hat), fixed_version (a fixed version recorded for an affected package, or the fixed bound of an affected version range), release_reference (a reference that EchelonGraph confirmed through the GitHub API is a published release containing the fix, or a pull request or commit that a published release contains), vendor_advisory_fix (a vendor advisory that lists a fix for the CVE under the vendor's own structured category: Red Hat's CSAF vendor_fix, Microsoft's Vendor Fix), nvd_patch_reference (an NVD reference tagged Patch), cna_patch_reference (a reference the CVE's CNA tagged patch in its CVE record), cna_solution (a sentence of the CVE record's solutions, written by its CNA, that names a fixed version; relayed as cna_solution)
|
|
2319
|
+
const PATCH_EVIDENCE_LABELS = ["vendor_patch", "fixed_version", "release_reference", "vendor_advisory_fix", "nvd_patch_reference", "cna_patch_reference", "cna_solution", "description_fix", "vendor_fix_released"];
|
|
2320
|
+
const PATCH_EVIDENCE_SOURCES = "vendor_patch (a vendor patch on record, from Ubuntu or Red Hat), fixed_version (a fixed version recorded for an affected package, or the fixed bound of an affected version range), release_reference (a reference that EchelonGraph confirmed through the GitHub API is a published release containing the fix, or a pull request or commit that a published release contains), vendor_advisory_fix (a vendor advisory that lists a fix for the CVE under the vendor's own structured category: Red Hat's CSAF vendor_fix, Microsoft's Vendor Fix), nvd_patch_reference (an NVD reference tagged Patch), cna_patch_reference (a reference the CVE's CNA tagged patch in its CVE record), cna_solution (a sentence of the CVE record's solutions or workarounds, written by its CNA, that names a fixed version; relayed as cna_solution), description_fix (a sentence of the CVE description that names a fixed version) and vendor_fix_released (the vendor states, in the CVE record, that a fix is released and the record names no fixed version; check the vendor's advisory)";
|
|
2280
2321
|
// #3070, #3076: what the CVE record's CNA wrote, as GET /cves/:id serves it (core-backend
|
|
2281
2322
|
// cna_record.go). One constant string each.
|
|
2282
2323
|
const CVE_CNA_ABSENT = "Absent where EchelonGraph has not read the CVE record's CNA container yet, where the record has none, for a REJECTED CVE, and from an API older than the field.";
|
|
@@ -2284,12 +2325,26 @@ const DATE_PUBLIC_FIELD = "When the vulnerability was made public, as the CVE re
|
|
|
2284
2325
|
CVE_CNA_ABSENT;
|
|
2285
2326
|
const DATE_PUBLIC_SOURCE_FIELD = "Whose date date_public is: cna, the CVE record's CNA. Absent whenever date_public is.";
|
|
2286
2327
|
const CNA_FIELD = "The CNA that wrote the CVE record: short_name as the record gives it, and name, the CVE Program's name for that CNA (the short name where its list does not hold it). " + CVE_CNA_ABSENT;
|
|
2287
|
-
const CNA_SOLUTION_FIELD = "The sentence of the CVE record's solutions, written by its CNA, that names a released fix, verbatim: what makes cna_solution patch evidence. It is the CNA's text, not EchelonGraph's advice. Absent where the record's solutions name no fixed version, as well as where the field itself is: " +
|
|
2328
|
+
const CNA_SOLUTION_FIELD = "The sentence of the CVE record's solutions or workarounds, written by its CNA, that names a released fix, verbatim: what makes cna_solution patch evidence. It is the CNA's text, not EchelonGraph's advice. cna_solution_section says which of the two sections it is. Absent where the record's solutions name no fixed version, as well as where the field itself is: " +
|
|
2288
2329
|
CVE_CNA_ABSENT;
|
|
2330
|
+
// #3178: which section cna_solution is. MCP relays a field its outputSchema does not describe without saying what it
|
|
2331
|
+
// is, so the section is described here, beside the sentence.
|
|
2332
|
+
const CNA_SOLUTION_SECTION_FIELD = "Which section of the CVE record cna_solution was read from: solution (the record's solutions) or workaround (its workarounds, read only where its solutions name no fix: a temporary measure the CNA lists beside its fix). A workaround's sentence is the record's workaround, never its solution. Present whenever cna_solution is; absent from an API older than the field.";
|
|
2289
2333
|
// #2842: what the descriptions say; the sources, and that patch_available once true is never set
|
|
2290
2334
|
// back, are the outputSchema's (PATCH_AVAILABLE_FIELD, PATCH_EVIDENCE_FIELD).
|
|
2291
|
-
const PATCH_AVAILABLE_DESCRIPTION = "patch_available is true when EchelonGraph holds
|
|
2292
|
-
|
|
2335
|
+
const PATCH_AVAILABLE_DESCRIPTION = "patch_available is true when EchelonGraph holds, or once held, fix evidence for the CVE; patch_evidence is what it holds now, strongest first. patch_available false means no fix evidence is on record, which is not a finding that no fix exists.";
|
|
2336
|
+
// #3138: true says evidence was held at some point, not that it is held now: the sources held now
|
|
2337
|
+
// are patch_evidence's, and true beside an empty one is a withdrawal (requiredText.ts
|
|
2338
|
+
// PATCH_AVAILABLE_WITHDRAWN, pinned there). #3138's review: round 1 changed the field text only,
|
|
2339
|
+
// and this sentence, in get_cve's and search_cves' descriptions, still said true means
|
|
2340
|
+
// EchelonGraph holds evidence; it now says "holds, or once held", in 9 more characters than it
|
|
2341
|
+
// had, as get_cve's description had 9 left under #2924's headroom of 100 below the 2,048 cut
|
|
2342
|
+
// (get_cve 1,948 characters, search_cves 1,807; test/prompts-resources.test.mjs holds the
|
|
2343
|
+
// headroom, test/client-cut.mjs (b) the cut, and marketing-site lib/mcpToolClaims.test.ts the
|
|
2344
|
+
// word "for" in get_cve's description, which its /pulse/mcp row uses).
|
|
2345
|
+
const PATCH_AVAILABLE_FIELD = "true: EchelonGraph holds, or once held, evidence of a fix for the CVE; patch_evidence names the sources it holds now. " +
|
|
2346
|
+
PATCH_AVAILABLE_WITHDRAWN +
|
|
2347
|
+
" false: no fix evidence is on record, which is not a finding that no fix exists. An answer with no patch_evidence (an API older than that field) does not name the evidence, and a false there is not a finding that no fix exists either.";
|
|
2293
2348
|
const PATCH_EVIDENCE_FIELD = "The sources of the fix evidence EchelonGraph holds for the CVE, strongest first: " +
|
|
2294
2349
|
PATCH_EVIDENCE_SOURCES +
|
|
2295
2350
|
". Empty where none was found when EchelonGraph last computed it. Absent (an API older than the field): the answer does not name the evidence.";
|
|
@@ -2340,6 +2395,8 @@ const CVERecord = z.looseObject({
|
|
|
2340
2395
|
date_public_source: opt(z.string()).describe(DATE_PUBLIC_SOURCE_FIELD),
|
|
2341
2396
|
cna: opt(z.looseObject({ short_name: opt(z.string()), name: opt(z.string()) })).describe(CNA_FIELD),
|
|
2342
2397
|
cna_solution: opt(z.string()).describe(CNA_SOLUTION_FIELD),
|
|
2398
|
+
// #3178: a section the API adds later is relayed, never a failed call (as patch_evidence's labels).
|
|
2399
|
+
cna_solution_section: opt(z.union([z.enum(["solution", "workaround"]), z.string()])).describe(CNA_SOLUTION_SECTION_FIELD),
|
|
2343
2400
|
references: z.unknown().optional(),
|
|
2344
2401
|
// #2720: NVD's configurations, losslessly, beside the flat list (see cpeConfigurationsNote).
|
|
2345
2402
|
cpe_match: z.unknown().optional().describe(CPE_MATCH_DESCRIPTION),
|
package/dist/prompts.js
CHANGED
|
@@ -49,6 +49,12 @@ const text = (t) => ({ messages: [{ role: "user", content: { type: "text", text:
|
|
|
49
49
|
// gives them as the vendor's, untested, and a not_parsed or none_in_source state as no finding that
|
|
50
50
|
// the vendor lists no fix. vendor_advisories_for_cve rows carry remediation_kinds too, so step 4
|
|
51
51
|
// sends the model to get_vendor_advisory where a row lists a workaround or a mitigation.
|
|
52
|
+
//
|
|
53
|
+
// Its exploit lines (#3039): cve_intel's exploits hold Nuclei detection templates beside exploit
|
|
54
|
+
// code, and a template checks whether a system is affected; it is not necessarily exploit code
|
|
55
|
+
// (core-backend egkev.ExploitCodeKinds, #3038). Until #3039 step 2 called the whole list "public
|
|
56
|
+
// exploit code" and the template asked for "public exploit code on record", so an agent reported
|
|
57
|
+
// exploit code for the ~2,800 CVEs EchelonGraph holds only a template for.
|
|
52
58
|
// #3013's review: the Patch line has the model state a vendor's workaround or mitigation step,
|
|
53
59
|
// whose wording is each item's text, and cve_remediation's cut can leave that text out of data
|
|
54
60
|
// (its second thinning level) or shorten it in the text (its TEXT CUT); no string is shortened in
|
|
@@ -58,14 +64,14 @@ export function triageCveText(cve) {
|
|
|
58
64
|
return [
|
|
59
65
|
`Triage ${cve} using the EchelonGraph MCP tools. Call them in this order, each with cve_id ${cve}:`,
|
|
60
66
|
"1. get_cve: the record. Read score_assessed before echelongraph_score (false: not yet scored, which is not a score of 0); read severity, cvss_v3_score, epss_score and epss_percentile, and the CISA-KEV fields kev_listed, kev_added_date, kev_due_date and kev_ransomware, and, where the record carries it, kev_required_action, CISA's own text.",
|
|
61
|
-
"2. cve_intel: weaknesses (cwes), public exploit code (exploits and
|
|
67
|
+
"2. cve_intel: weaknesses (cwes), public exploit code and detection templates (exploits, exploits_total and exploits_by_kind; a nuclei_template row is a Nuclei detection template, which checks whether a system is affected and is not necessarily exploit code; verified_status is the label stored with each reference, not a guarantee that it works, and an empty list is not evidence that no exploit exists), affected_packages (each package's affected ranges with their fixes in fixed_branches) and fixed_versions.",
|
|
62
68
|
"3. cve_remediation: how it is fixed, as its sources state it: fixed_branches (each affected package's ranges with their fixes), vendor_remediations (each vendor advisory naming the CVE, with its vendor, vendor_advisory_id, remediation_state and remediation_kinds, each kind the vendor's own category) and fix_references (the record's references tagged Patch or Mitigation).",
|
|
63
69
|
"4. vendor_advisories_for_cve: the vendor advisories that name it, with remediation_kinds and remediation_state where a row carries them. Call get_vendor_advisory for one whose remediation you need in full, such as one whose remediation_kinds include workaround or mitigation.",
|
|
64
70
|
"5. epss_history: how its EPSS score changed, one point per recorded change (series_kind change_only): before series_starts_at a missing point means not recorded, not unchanged.",
|
|
65
71
|
"6. cve_exposure: internet exposure on record. exposure_state says what the count is; exposed_hosts counts distinct ip:port services, not machines; the result is not_assessed with measured_at null, so present any count as undated and on record, and exposure_state not_assessed as not assessed.",
|
|
66
72
|
ENVELOPE_RULES,
|
|
67
73
|
"Then answer with this template, one line each, naming the tool and the measured_at behind every line:",
|
|
68
|
-
"- Exploited? CISA-KEV listing and its date, known ransomware use, and public exploit code on record.",
|
|
74
|
+
"- Exploited? CISA-KEV listing and its date, known ransomware use, and public exploit code on record (Metasploit modules, Exploit-DB entries, GitHub or vendor proofs of concept); give Nuclei detection templates apart, as detection checks, never as exploit code.",
|
|
69
75
|
"- Likelihood: EPSS score and percentile, and how it has moved.",
|
|
70
76
|
"- Reachable? exposure_state and the count on record, or not assessed.",
|
|
71
77
|
`- Patch: fixed versions, vendor remediation and fix references; where the results hold none, write "none on record", which is not a finding that no fix exists. For each affected package whose cve_intel row carries fixed_branches, give every range with its fix: from introduced ("0" is the first version) up to its fixed, or up to and including last_affected, where no fix is on record for that range; never one range's fix as the fix for every affected branch or the version every install should move to. cve_intel keeps one fixed_version per ecosystem and package, its advisory's last range's as a rule, which need not be the fix for every affected version of the package: where a row has no fixed_branches (null or absent), give fixed_version as the fixed version cve_intel records for that package, never as the fix for every affected branch, and for the fix on a given branch point to get_cve's references tagged Vendor Advisory or Patch, where it has them, and to the vendor advisories. From cve_remediation, give each vendor_remediations advisory by vendor and vendor_advisory_id with its remediation_kinds, a workaround or mitigation as the step that vendor states, which EchelonGraph has not tested and which is not a fix, and its fix_references; a remediation_state of not_parsed or none_in_source is not a finding that the vendor lists no fix. ${VENDOR_STEP_ELSEWHERE}`,
|
package/dist/requiredText.js
CHANGED
|
@@ -116,3 +116,18 @@ export const REQUIRED_IN_HEAD = {
|
|
|
116
116
|
get_vendor_advisory: ["withdrawn (true: the vendor rescinded it", "which are not active vulnerabilities"],
|
|
117
117
|
search_vendor_advisories: ["never exactly 1,000", "The query is sent in a request header, never in the URL.", "which are not active vulnerabilities"],
|
|
118
118
|
};
|
|
119
|
+
// #3138: sentences a tool's outputSchema must carry, by the path of the field in its success
|
|
120
|
+
// branch (a dot between an object and a field inside it, [] for each entry of a list).
|
|
121
|
+
// test/client-cut.mjs (e) holds every one to tools/list's outputSchema, with a control in
|
|
122
|
+
// tools.test.mjs. Through 3.0.0 patch_available's field text said true means "EchelonGraph holds
|
|
123
|
+
// evidence of a fix for the CVE, its sources named in patch_evidence", while #2861's rule never sets
|
|
124
|
+
// it back to false, so every withdrawal of evidence leaves true beside patch_evidence [] (hosted
|
|
125
|
+
// get_cve CVE-2026-1281, 2026-10-11: true, [], and remediation.fix_status none_confirmed), and an
|
|
126
|
+
// agent reading the field through its schema reported a fix on record where EchelonGraph held
|
|
127
|
+
// none. The sentence says what that pair means and which field is the current reading; it names a
|
|
128
|
+
// field, not a tool (test/description-policy.test.mjs: no schema text names another tool).
|
|
129
|
+
export const PATCH_AVAILABLE_WITHDRAWN = "true beside an empty patch_evidence means evidence of a fix once held has since been withdrawn (patch_available is never set back to false), and fix_status (remediation.fix_status in the CVE's record) is the current reading, not patch_available.";
|
|
130
|
+
export const REQUIRED_IN_OUTPUT_SCHEMA = {
|
|
131
|
+
get_cve: { "data.patch_available": [PATCH_AVAILABLE_WITHDRAWN] },
|
|
132
|
+
search_cves: { "data.cves[].patch_available": [PATCH_AVAILABLE_WITHDRAWN] },
|
|
133
|
+
};
|