echelongraph-mcp 2.7.2 → 3.0.0
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 +124 -0
- package/README.md +146 -432
- package/dist/fixStatus.js +28 -0
- package/dist/index.js +225 -29
- package/dist/prompts.js +55 -31
- package/dist/requiredText.js +1 -1
- package/dist/resources.js +32 -6
- package/dist/textBudget.js +328 -16
- package/dist/tools/check_affected.js +41 -11
- package/dist/tools/check_sbom.js +47 -8
- package/dist/tools/cve_intel.js +15 -6
- package/dist/tools/cve_remediation.js +156 -16
- package/dist/tools/epss_history.js +6 -3
- package/dist/tools/get_cwe.js +7 -2
- package/dist/tools/kev_recent.js +18 -2
- package/dist/tools/match_batch.js +10 -6
- package/dist/tools/scan_manifest.js +20 -7
- package/dist/tools/vendor_advisories.js +34 -7
- 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 +163 -0
- package/package.json +2 -1
- package/server.json +2 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// fix_status (#2841, #2518 item 3): the API's fix-status rule for a CVE (core-backend
|
|
2
|
+
// internal/cve/fixstatus.go, the rule CVE Pulse pages render), as get_cve's remediation summary
|
|
3
|
+
// (its first field) and cve_remediation's answer carry it. Relayed, never derived here.
|
|
4
|
+
//
|
|
5
|
+
// A module of its own, importing only zod: index.ts and tools/cve_remediation.ts import each
|
|
6
|
+
// other, so a const shared between them would be read before it is initialised whichever loads
|
|
7
|
+
// first.
|
|
8
|
+
import * as z from "zod";
|
|
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.";
|
|
11
|
+
const opt = (t) => t.nullable().optional();
|
|
12
|
+
export function fixStatusSchema() {
|
|
13
|
+
return z
|
|
14
|
+
.looseObject({
|
|
15
|
+
// A state a later API adds is admitted, never a failed call.
|
|
16
|
+
state: opt(z.union([z.enum(FIX_STATES), z.string()])),
|
|
17
|
+
basis: opt(z.string()),
|
|
18
|
+
described_by: opt(z.string()),
|
|
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()) })),
|
|
21
|
+
stated_in_record: opt(z.boolean()),
|
|
22
|
+
listed_in_patch_table: opt(z.boolean()),
|
|
23
|
+
unread_inputs: opt(z.array(z.string())),
|
|
24
|
+
})
|
|
25
|
+
.nullable()
|
|
26
|
+
.optional()
|
|
27
|
+
.describe(FIX_STATUS_FIELD);
|
|
28
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
// what it left out (readKEVExposure, readExposedDatabases, readLeakedCredentials,
|
|
17
17
|
// readShadowAI and readMCPServers; #2307, #2313, #2315). content[2] is the envelope below,
|
|
18
18
|
// as JSON, less what content[0] and the note already say. Past 30,000 characters of JSON,
|
|
19
|
-
// content[0] is that JSON cut to fit and the note ends by saying how (TEXT CUT);
|
|
20
|
-
// envelope's data is
|
|
19
|
+
// content[0] is that JSON cut to fit and the note ends by saying how (TEXT CUT, #2783); past
|
|
20
|
+
// 30,000 characters of structured result, the envelope's data is cut too, unless the call
|
|
21
|
+
// says full: true, and the note says how (DATA CUT, #3013; both in textBudget.ts).
|
|
21
22
|
// - failure (isError: true): the lookup did not complete — unreachable host, non-2xx,
|
|
22
23
|
// timeout, or a 2xx whose body is not a JSON object — named by tool, cause and base URL,
|
|
23
24
|
// in content[0]; content[1] is the envelope below, as JSON, less the message's sentences.
|
|
@@ -27,18 +28,23 @@
|
|
|
27
28
|
// Structured results (#2311, #2313). Every result, success or failure, also carries
|
|
28
29
|
// structuredContent: one envelope that says how the answer was measured — state (measured,
|
|
29
30
|
// not_assessed, failed, invalid_input), measured_at, method, coverage, freshness and notes —
|
|
30
|
-
// with, on a success, `data`, the answer
|
|
31
|
-
//
|
|
32
|
-
// CUT, #2783), so past them content[0] is not equal to `data` (#2802).
|
|
33
|
-
//
|
|
31
|
+
// with, on a success, `data`, the answer, and on a failure an `error`. content[0] carries the
|
|
32
|
+
// answer as JSON up to 30,000 characters, and past them a cut of it that the note names (TEXT
|
|
33
|
+
// CUT, #2783), so past them content[0] is not equal to `data` (#2802). `data` is the answer whole
|
|
34
|
+
// unless the whole structured result would pass 30,000 characters of JSON: then, unless the call
|
|
35
|
+
// says full: true, its lists are cut in the order the tool names (DATA CUT, #3013), data_truncated
|
|
36
|
+
// is true and truncated gives each list cut (data_truncated is false otherwise). Each tool
|
|
37
|
+
// advertises the envelope as its outputSchema, which `data` validates against cut or whole.
|
|
34
38
|
//
|
|
35
39
|
// The envelope in the text (#2440, #2467). Many clients pass only `content` to the model, so the
|
|
36
40
|
// last text block of every result is structuredContent serialized as JSON, less what an earlier
|
|
37
41
|
// text block already carries: `data` (content[0] on a success, whole or cut as above), the note's
|
|
38
42
|
// sentences (the block just before, which structuredContent's notes end with), and method where
|
|
39
43
|
// that note quotes it. The rest — state, measured_at, coverage, freshness, the envelope's own notes
|
|
40
|
-
// and, on a failure, error — is copied key for key, so
|
|
41
|
-
// structuredContent, but for what a TEXT CUT leaves out
|
|
44
|
+
// and, on a failure, error; on a success data_truncated and truncated — is copied key for key, so
|
|
45
|
+
// the text blocks together are the whole of structuredContent, but for what a TEXT CUT leaves out
|
|
46
|
+
// of the answer, and cannot disagree with it. Beside a DATA CUT (#3013) content[0] is still cut from
|
|
47
|
+
// the answer whole, not from the cut data, so it can hold entries data leaves out. The
|
|
42
48
|
// MCP spec (2025-06-18, Tools, Structured Content) asks a tool that returns structured content to
|
|
43
49
|
// return it serialized in a text block too; the JSON block stays first and the note second, as in
|
|
44
50
|
// 1.x, so a client that parses content[0] still parses the answer's JSON, which past 30,000
|
|
@@ -59,12 +65,13 @@ import { registerCheckSbom } from "./tools/check_sbom.js";
|
|
|
59
65
|
import { registerScanManifest } from "./tools/scan_manifest.js";
|
|
60
66
|
import { registerCveIntel } from "./tools/cve_intel.js";
|
|
61
67
|
import { registerCveRemediation } from "./tools/cve_remediation.js";
|
|
68
|
+
import { fixStatusSchema } from "./fixStatus.js";
|
|
62
69
|
import { registerGetCwe } from "./tools/get_cwe.js";
|
|
63
70
|
import { holdRequest, isHttpEntrypoint, requestSignal, upstreamHeaders, upstreamUserAgent } from "./runtime.js";
|
|
64
71
|
import { registerVendorAdvisoryTools } from "./tools/vendor_advisories.js";
|
|
65
72
|
import { registerPrompts } from "./prompts.js";
|
|
66
73
|
import { registerResources } from "./resources.js";
|
|
67
|
-
import { dataText, GET_CVE_WHOLE, TEXT_BUDGET_DESCRIPTION } from "./textBudget.js";
|
|
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";
|
|
68
75
|
import { SHODAN_ATTRIBUTION, SHODAN_OWNERSHIP } from "./requiredText.js";
|
|
69
76
|
// The package actually running, read from the package.json that ships beside dist/. The MCP
|
|
70
77
|
// handshake (serverInfo) and the User-Agent both carry its name and version, so the API's
|
|
@@ -372,12 +379,34 @@ export const checked = (tool, schema, r) => {
|
|
|
372
379
|
};
|
|
373
380
|
// A success: content[0] is the data (cut to DATA_TEXT_BUDGET when it is larger, #2783), content[1]
|
|
374
381
|
// the note, content[2] the envelope less both; the envelope carries both, the note as sentences
|
|
375
|
-
// after any the envelope adds about itself. structuredContent.data is the data whole
|
|
382
|
+
// after any the envelope adds about itself. structuredContent.data is the data whole, or (#3013),
|
|
383
|
+
// when the structured result would pass STRUCTURED_BUDGET and the call (answering()) does not say
|
|
384
|
+
// full: true, cut as the call's DataCut says, with data_truncated, truncated and the note's DATA CUT
|
|
385
|
+
// sentences saying how. content[0] is cut from the data whole either way.
|
|
376
386
|
export const succeeded = (data, note, env, cut) => {
|
|
377
387
|
const own = env.notes ?? [];
|
|
388
|
+
const call = answeringNow();
|
|
378
389
|
const shown = dataText(data, cut);
|
|
379
|
-
const
|
|
380
|
-
|
|
390
|
+
const structuredOf = (d, said, truncated) => ({
|
|
391
|
+
...env,
|
|
392
|
+
notes: [...own, ...sentences(said)],
|
|
393
|
+
data_truncated: truncated !== undefined,
|
|
394
|
+
...(truncated === undefined ? {} : { truncated }),
|
|
395
|
+
data: d,
|
|
396
|
+
});
|
|
397
|
+
let said = shown.said ? `${note} ${shown.said}` : note;
|
|
398
|
+
let structuredContent = structuredOf(data, said);
|
|
399
|
+
const wholeChars = JSON.stringify(structuredContent).length;
|
|
400
|
+
if (call?.full !== true && wholeChars > STRUCTURED_BUDGET) {
|
|
401
|
+
// The same text, with the sentences a first text block beside a cut data says.
|
|
402
|
+
const beside = dataText(data, cut, FIRST_TEXT_BLOCK_BESIDE_A_CUT).said;
|
|
403
|
+
const head = beside ? `${note} ${beside}` : note;
|
|
404
|
+
const c = cutData(data, call?.cut ?? NO_DATA_CUT, wholeChars, (d, truncated, dataSaid) => structuredOf(d, `${head} ${dataSaid}`, truncated));
|
|
405
|
+
if (c) {
|
|
406
|
+
said = `${head} ${c.said}`;
|
|
407
|
+
structuredContent = c.structured;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
381
410
|
return { content: [text(shown.text), text(said), envelopeText(structuredContent, said, own)], structuredContent };
|
|
382
411
|
};
|
|
383
412
|
export const okHead = (tool, status) => `${tool} OK: EchelonGraph answered${status === undefined ? "" : ` HTTP ${status}`} from ${SHOWN_BASE}.`;
|
|
@@ -663,6 +692,35 @@ const SEARCH_CVES_TEXT = {
|
|
|
663
692
|
whole: `${GET_CVE_WHOLE}.`,
|
|
664
693
|
page: (shown, _rows, d) => `To read the rows left out in the text, call search_cves again with the same arguments and offset ${(numAt(d, "offset") ?? 0) + shown}; a page of ${shown} rows or fewer like these fits the text without leaving rows out.`,
|
|
665
694
|
};
|
|
695
|
+
// #3013: the structured result's cut. A row of production's answer is about 4,200 characters of
|
|
696
|
+
// compact JSON (score_factors, cpe_match and references, about 2,000 of them, are get_cve's), so the
|
|
697
|
+
// default page of 20 was 85,383 characters of structured result. Before any row leaves, every row
|
|
698
|
+
// is thinned: first to the first text cut level's fields, its description whole; then to the fields
|
|
699
|
+
// the description names, without the description. Only then do rows leave data from the end, and
|
|
700
|
+
// the note gives the offset to read them from, and the page size whose rows fit data whole. Through
|
|
701
|
+
// #3013's review that size was the number of thinned rows data kept, so a page of it came back
|
|
702
|
+
// thinned again (data_truncated true) while the note said it fit data whole: it is now counted on
|
|
703
|
+
// the rows whole (PageAt.wholeFit), the same call's envelope and note beside them.
|
|
704
|
+
const SEARCH_CVES_DATA = {
|
|
705
|
+
steps: [
|
|
706
|
+
{
|
|
707
|
+
lists: ["cves"],
|
|
708
|
+
thin: [
|
|
709
|
+
[
|
|
710
|
+
"cve_id", "description", "severity", "cvss_v3_score", "echelongraph_score", "echelongraph_severity", "echelongraph_risk", "score_assessed", "score_unassessed_reason", "epss_score",
|
|
711
|
+
"kev_listed", "kev_ransomware", "published", "vuln_status", "patch_available", "patch_evidence", "exploit_poc_available",
|
|
712
|
+
],
|
|
713
|
+
["cve_id", "severity", "cvss_v3_score", "echelongraph_score", "score_assessed", "score_unassessed_reason", "epss_score", "kev_listed", "kev_ransomware", "published", "patch_available", "patch_evidence"],
|
|
714
|
+
],
|
|
715
|
+
},
|
|
716
|
+
],
|
|
717
|
+
page: (t, d, at) => {
|
|
718
|
+
const k = t.cves?.returned;
|
|
719
|
+
if (!k || !t.cves || k === t.cves.total)
|
|
720
|
+
return "";
|
|
721
|
+
return `To read the rows past the first ${k} in data, call search_cves again with the same arguments and offset ${(numAt(d, "offset") ?? 0) + k}; ${wholePage(at.wholeFit("cves"))}`;
|
|
722
|
+
},
|
|
723
|
+
};
|
|
666
724
|
async function searchCVEs(a) {
|
|
667
725
|
const tool = "search_cves";
|
|
668
726
|
try {
|
|
@@ -751,10 +809,73 @@ function kevTextNote(rec) {
|
|
|
751
809
|
return "";
|
|
752
810
|
const has = ["kev_required_action", "kev_short_description", "kev_notes_urls"].some((k) => field(rec, k) !== undefined && field(rec, k) !== null);
|
|
753
811
|
return has
|
|
754
|
-
? " kev_required_action, kev_short_description and kev_notes_urls are CISA's own text from its KEV catalog, relayed verbatim: CISA's required action is directed at US federal civilian agencies under Binding Operational Directive 22-01 and is not EchelonGraph's advice."
|
|
812
|
+
? " kev_required_action, kev_short_description and kev_notes_urls are CISA's own text from its KEV catalog, relayed verbatim: CISA's required action is directed at US federal civilian agencies under Binding Operational Directive 26-04 (which replaced BOD 22-01 on 10 June 2026) and is not EchelonGraph's advice."
|
|
755
813
|
: "";
|
|
756
814
|
}
|
|
815
|
+
// #2880: what the record's CISA BOD 26-04 timeline is, where the scorer wrote it
|
|
816
|
+
// (core-backend/internal/cve/scoring/bod2604.go, algorithm v3.4+): given for both answers to "is
|
|
817
|
+
// the system publicly exposed?", because that is the reader's own system's, each input with its
|
|
818
|
+
// source. A record scored before v3.4, or a malformed block, gets no sentence: the JSON is relayed
|
|
819
|
+
// whole either way. What the SSVC decision is (EchelonGraph's run of CISA's table, never a
|
|
820
|
+
// decision CISA published, #3053) is in the outputSchema's echelongraph_ssvc_decision, not the
|
|
821
|
+
// note: get_cve's text is measured to the character (test/text-bound.mjs).
|
|
822
|
+
const BOD_TIMELINE = {
|
|
823
|
+
FSU: "Fix on system upgrade",
|
|
824
|
+
"60D": "60 days",
|
|
825
|
+
"14D": "14 days",
|
|
826
|
+
"3D": "3 days",
|
|
827
|
+
"3DF": "3 days & forensic triage",
|
|
828
|
+
};
|
|
829
|
+
const BOD_SOURCE = {
|
|
830
|
+
"cisa-adp": "CISA's Vulnrichment value",
|
|
831
|
+
cisa_kev: "CISA's KEV catalog",
|
|
832
|
+
cvss_v4_au: "the CVSS v4.0 Automatable metric",
|
|
833
|
+
cvss_v4_av_physical: "CVSS v4.0 attack vector Physical",
|
|
834
|
+
cvss_v4_vc_vi_high: "CVSS v4.0 confidentiality and integrity impact both High",
|
|
835
|
+
cvss_v4_vc_vi_not_high: "CVSS v4.0 confidentiality and integrity impact neither High",
|
|
836
|
+
cisa_bod_26_04_default: "CISA's BOD 26-04 default for a CVE not in KEV that nobody has assessed",
|
|
837
|
+
not_supplied: "not yet published by CISA, so every timeline it allows is listed",
|
|
838
|
+
};
|
|
839
|
+
function bodTimelines(v) {
|
|
840
|
+
if (!Array.isArray(v) || v.length === 0 || !v.every((k) => typeof k === "string" && BOD_TIMELINE[k]))
|
|
841
|
+
return null;
|
|
842
|
+
const w = v.map((k) => BOD_TIMELINE[k]);
|
|
843
|
+
return w.length === 1 ? w[0] : `${w.slice(0, -1).join(", ")} or ${w[w.length - 1]}`;
|
|
844
|
+
}
|
|
845
|
+
function bodNote(cve, rec) {
|
|
846
|
+
const b = field(rec, "score_factors", "bod_26_04");
|
|
847
|
+
const exposed = bodTimelines(field(b, "if_publicly_exposed"));
|
|
848
|
+
const internal = bodTimelines(field(b, "if_not_publicly_exposed"));
|
|
849
|
+
const kev = strAt(b, "kev", "value");
|
|
850
|
+
const input = (k, name) => {
|
|
851
|
+
const v = strAt(b, k, "value");
|
|
852
|
+
const src = strAt(b, k, "source");
|
|
853
|
+
return v && src ? `${name} ${v} (${BOD_SOURCE[src] ?? src})` : null;
|
|
854
|
+
};
|
|
855
|
+
const auto = input("automatable", "automatable");
|
|
856
|
+
const ti = input("technical_impact", "technical impact");
|
|
857
|
+
if (!(exposed && internal && (kev === "yes" || kev === "no") && auto && ti))
|
|
858
|
+
return "";
|
|
859
|
+
return ` score_factors.bod_26_04 is CISA BOD 26-04's remediation timeline for ${cve}: ${exposed} if the affected system is publicly exposed, ${internal} if it is not; whether a system is publicly exposed is its owner's to say. Its inputs: in CISA KEV ${kev}, ${auto}, ${ti}. BOD 26-04 binds US Federal Civilian Executive Branch agencies; for anyone else it is CISA's published timeline, not EchelonGraph's advice.`;
|
|
860
|
+
}
|
|
757
861
|
// #2841: one sentence on the record's remediation summary, where the API sends one.
|
|
862
|
+
// #3070, #3076: the CNA's own date and fix sentence, named as the CNA's. Nothing when the record
|
|
863
|
+
// carries neither; a value of another JSON type is not quoted (the outputSchema refuses it).
|
|
864
|
+
function cnaRecordNote(cve, rec) {
|
|
865
|
+
const cna = field(rec, "cna");
|
|
866
|
+
const nameOf = (k) => (cna && typeof cna === "object" && typeof field(cna, k) === "string" ? String(field(cna, k)).trim() : "");
|
|
867
|
+
const who = nameOf("name") || nameOf("short_name");
|
|
868
|
+
// An appositive name is closed by a comma: "its CNA, Palo Alto Networks, Inc., states".
|
|
869
|
+
const by = who ? `its CNA, ${who},` : "its CNA";
|
|
870
|
+
const date = strAt(rec, "date_public");
|
|
871
|
+
const fix = strAt(rec, "cna_solution");
|
|
872
|
+
const parts = [];
|
|
873
|
+
if (date)
|
|
874
|
+
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)
|
|
876
|
+
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.`);
|
|
877
|
+
return parts.join("");
|
|
878
|
+
}
|
|
758
879
|
function remediationSummaryNote(cve, rec) {
|
|
759
880
|
const s = field(rec, "remediation");
|
|
760
881
|
if (s === null || typeof s !== "object")
|
|
@@ -764,8 +885,21 @@ function remediationSummaryNote(cve, rec) {
|
|
|
764
885
|
const failed = field(s, "failed_sections");
|
|
765
886
|
const notRead = Array.isArray(failed) && failed.length ? ` The summary could not read ${failed.join(", ")} (failed_sections), so a missing count there is not a finding of none.` : "";
|
|
766
887
|
const byKind = counted.length ? ` Vendor advisories list ${counted.map(([k, n]) => `${k} (${n})`).join(", ")}, by the vendor's own category.` : "";
|
|
767
|
-
|
|
888
|
+
// #2841: fix_status is the API's, relayed; a state is named only as the API sent it.
|
|
889
|
+
const fs = field(s, "fix_status");
|
|
890
|
+
const state = strAt(fs, "state");
|
|
891
|
+
const basis = strAt(fs, "basis");
|
|
892
|
+
const fix = state
|
|
893
|
+
? ` remediation.fix_status is ${state}${basis ? ` (basis ${basis})` : ""}, the API's fix-status rule${state === "none_confirmed" ? ", which is not a finding that no fix exists" : ""}.`
|
|
894
|
+
: "";
|
|
895
|
+
return ` remediation summarises how ${cve} is fixed, as its sources state it; cve_remediation returns each vendor's text, CISA's action and every fixed range.${fix}${byKind}${notRead}`;
|
|
768
896
|
}
|
|
897
|
+
// #3013: the structured result's cut. CVE-2021-44228's record is 64,170 characters of structured
|
|
898
|
+
// result, 41,091 of them cpe_match (396 entries) and 17,103 references (103): those two are cut
|
|
899
|
+
// first, each to at most the same number of its first entries, then cpe_configurations, then any
|
|
900
|
+
// other list at the top level of the record. The remediation summary (#2841), an object, is never
|
|
901
|
+
// cut, so its fix_status is whole at every level.
|
|
902
|
+
const GET_CVE_DATA = { steps: [{ lists: ["cpe_match", "references"] }, { lists: ["cpe_configurations"] }] };
|
|
769
903
|
async function getCVE(cve_id) {
|
|
770
904
|
const tool = "get_cve";
|
|
771
905
|
const id = cve_id.trim();
|
|
@@ -777,7 +911,7 @@ async function getCVE(cve_id) {
|
|
|
777
911
|
return failed(tool, r);
|
|
778
912
|
const written = strAt(r.data, "updated_at");
|
|
779
913
|
const cve = strAt(r.data, "cve_id") ?? id;
|
|
780
|
-
return succeeded(r.data, `${okHead(tool, r.status)} Returned the record for ${cve}.${cveScoreNote(cve, r.data)}${kevTextNote(r.data)}${remediationSummaryNote(cve, r.data)}`, {
|
|
914
|
+
return succeeded(r.data, `${okHead(tool, r.status)} Returned the record for ${cve}.${cveScoreNote(cve, r.data)}${bodNote(cve, r.data)}${kevTextNote(r.data)}${cnaRecordNote(cve, r.data)}${remediationSummaryNote(cve, r.data)}`, {
|
|
781
915
|
state: "measured",
|
|
782
916
|
measured_at: instantOrNull(written),
|
|
783
917
|
method: CVE_FEED_METHOD,
|
|
@@ -1960,6 +2094,35 @@ const RADAR_METHOD = "Aggregate counts over what each radar holds on record: kev
|
|
|
1960
2094
|
export const RADAR_NAMES = RADARS.map(([name]) => name);
|
|
1961
2095
|
const RADAR_STATE_NOTE = "state is not_assessed: every radar answered, but none gives one time at which what it counts was observed: kev_exposure, exposed_databases, leaked_credentials, shadow_ai's confirmed_exposed and mcp_servers date what they count at most by a window (kev_exposure.window, exposed_databases.window, leaked_credentials.window, shadow_ai.confirmed_exposed.window, mcp_servers.window), from the oldest observation among the rows counted to the newest, so no count here is presented as a dated measurement and measured_at is null. Each count is what that radar holds on record, and the note labels each one by what it counts.";
|
|
1962
2096
|
const RADAR_FRESHNESS_NOTE = "freshness gives each radar's last completed check where the API serves one (freshness.kev_exposure.last_run_at, freshness.exposed_databases.last_run_at, freshness.leaked_credentials.last_run_at, freshness.shadow_ai.last_run_at with freshness.shadow_ai.running, and freshness.mcp_servers.last_run_at with freshness.mcp_servers.enabled), and null where it does not.";
|
|
2097
|
+
// #3013: the structured result's cut. Production's answers of 2026-10-05 (fixtures/prod-answers) are
|
|
2098
|
+
// 33,278 characters of structured result, past the budget by notes that label every number (about
|
|
2099
|
+
// 20,000 characters) beside 12,733 of data. The notes say what each list counts, from the answer
|
|
2100
|
+
// whole, so the lists are cut, never a note: first the two series (shadow_ai.observed.trend_30d,
|
|
2101
|
+
// kev_exposure.trend), then every ranking to at most the same number of its first rows, then
|
|
2102
|
+
// kev_exposure.newest_kev, whose CVEs and exposure states the note names one by one. #3013's
|
|
2103
|
+
// review: the series arrive oldest first (a day, a week a row), and a cut that kept their first
|
|
2104
|
+
// entries showed the oldest, lowest weeks as the trend and left out the latest; each now keeps its
|
|
2105
|
+
// newest entries (keepNewest), and the note says so.
|
|
2106
|
+
const EXPOSURE_RADAR_DATA = {
|
|
2107
|
+
steps: [
|
|
2108
|
+
{ lists: ["shadow_ai.observed.trend_30d", "kev_exposure.trend"], keepNewest: true },
|
|
2109
|
+
{
|
|
2110
|
+
lists: [
|
|
2111
|
+
"kev_exposure.top_products",
|
|
2112
|
+
"kev_exposure.top_countries",
|
|
2113
|
+
"kev_exposure.top_cves",
|
|
2114
|
+
"exposed_databases.top_engines",
|
|
2115
|
+
"exposed_databases.top_countries",
|
|
2116
|
+
"leaked_credentials.top_providers",
|
|
2117
|
+
"leaked_credentials.top_types",
|
|
2118
|
+
"shadow_ai.observed.top_products",
|
|
2119
|
+
"shadow_ai.observed.top_countries",
|
|
2120
|
+
"shadow_ai.observed.top_issuers",
|
|
2121
|
+
],
|
|
2122
|
+
},
|
|
2123
|
+
{ lists: ["kev_exposure.newest_kev"] },
|
|
2124
|
+
],
|
|
2125
|
+
};
|
|
1963
2126
|
async function exposureRadar() {
|
|
1964
2127
|
const tool = "exposure_radar";
|
|
1965
2128
|
try {
|
|
@@ -2059,6 +2222,10 @@ export const NOTES = z.array(z.string()).describe("Caveats, one sentence each.")
|
|
|
2059
2222
|
// says both, and leaves what each count is to the notes and exposure_state, which say it per
|
|
2060
2223
|
// answer; it makes no blanket claim about the numbers, in either direction.
|
|
2061
2224
|
const SUCCESS_STATE = "measured: a measurement of what was asked; an exposure count is measured only with measured_at and method. not_assessed: the answer holds no dated measurement of what was asked, so no count in it is presented as one; it can still relay a count, as what the source holds on record, undated, and its notes (and exposure_state, where the result carries it) say what each count is.";
|
|
2225
|
+
// #3013: what a success says of its data's cut (textBudget.ts cutData), one constant string each.
|
|
2226
|
+
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
|
+
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
|
+
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() }));
|
|
2062
2229
|
export function envelopeSchema(o) {
|
|
2063
2230
|
const coverage = o.coverage ? o.coverage.nullable() : z.null();
|
|
2064
2231
|
return z.discriminatedUnion("state", [
|
|
@@ -2070,6 +2237,8 @@ export function envelopeSchema(o) {
|
|
|
2070
2237
|
freshness: (o.freshness ? o.freshness.nullable() : z.null()).describe("The producing radar's last completed check (last_run_at), where the API serves one."),
|
|
2071
2238
|
notes: NOTES,
|
|
2072
2239
|
...(o.extra ?? {}),
|
|
2240
|
+
data_truncated: z.boolean().describe(DATA_TRUNCATED_FIELD),
|
|
2241
|
+
truncated: TRUNCATED.optional().describe(TRUNCATED_FIELD),
|
|
2073
2242
|
data: o.data,
|
|
2074
2243
|
}),
|
|
2075
2244
|
z.strictObject({
|
|
@@ -2100,9 +2269,23 @@ const CPE_CONFIGURATIONS_DESCRIPTION = "NVD's configurations array as NVD sent i
|
|
|
2100
2269
|
// the GitHub API, a published release holding the fix; its HTML heuristics, the CISA KEV inference
|
|
2101
2270
|
// among them, are not evidence) and cna_patch_reference (the CNA's own CVE record tags a reference
|
|
2102
2271
|
// "patch"; NVD's capitalised "Patch" stays nvd_patch_reference).
|
|
2272
|
+
// #3076 / #2518 (rule 3): vendor_advisory_fix (a vendor's own structured "vendor fix" for the CVE:
|
|
2273
|
+
// Red Hat CSAF, Microsoft CVRF), cna_solution and description_fix (a sentence of the CVE record's
|
|
2274
|
+
// solutions, or of its description, that states a released fix in core-backend's fix-statement
|
|
2275
|
+
// templates, internal/cve/fix_statement.go). The last two are the record's own words: get_cve
|
|
2276
|
+
// relays the solution's sentence as cna_solution, and the description is in the record.
|
|
2103
2277
|
// One constant string each (the site's tool-claims check folds only constant strings).
|
|
2104
|
-
const PATCH_EVIDENCE_LABELS = ["vendor_patch", "fixed_version", "release_reference", "nvd_patch_reference", "cna_patch_reference"];
|
|
2105
|
-
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), nvd_patch_reference (an NVD reference tagged Patch)
|
|
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) and description_fix (a sentence of the CVE description that names a fixed version)";
|
|
2280
|
+
// #3070, #3076: what the CVE record's CNA wrote, as GET /cves/:id serves it (core-backend
|
|
2281
|
+
// cna_record.go). One constant string each.
|
|
2282
|
+
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.";
|
|
2283
|
+
const DATE_PUBLIC_FIELD = "When the vulnerability was made public, as the CVE record's CNA states it (the record's datePublic: \"if known, the date/time the vulnerability was disclosed publicly\"). It is the CNA's date, not the record's own publication date, which is published. " +
|
|
2284
|
+
CVE_CNA_ABSENT;
|
|
2285
|
+
const DATE_PUBLIC_SOURCE_FIELD = "Whose date date_public is: cna, the CVE record's CNA. Absent whenever date_public is.";
|
|
2286
|
+
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: " +
|
|
2288
|
+
CVE_CNA_ABSENT;
|
|
2106
2289
|
// #2842: what the descriptions say; the sources, and that patch_available once true is never set
|
|
2107
2290
|
// back, are the outputSchema's (PATCH_AVAILABLE_FIELD, PATCH_EVIDENCE_FIELD).
|
|
2108
2291
|
const PATCH_AVAILABLE_DESCRIPTION = "patch_available is true when EchelonGraph holds evidence of a fix for the CVE, and patch_evidence names its sources, strongest first. patch_available false means no fix evidence is on record, which is not a finding that no fix exists.";
|
|
@@ -2114,14 +2297,14 @@ const PATCH_EVIDENCE_FIELD = "The sources of the fix evidence EchelonGraph holds
|
|
|
2114
2297
|
// while kev_listed. Relayed as CISA's, verbatim; the required action is CISA's directive to US
|
|
2115
2298
|
// federal civilian agencies, not EchelonGraph's advice. Constant strings, like the others here.
|
|
2116
2299
|
const KEV_TEXT_ABSENT = "Absent where the CVE is not CISA-KEV-listed, where CISA's entry has none, or where EchelonGraph has not stored it yet (an API older than the field serves none).";
|
|
2117
|
-
const KEV_REQUIRED_ACTION_FIELD = "CISA's requiredAction for the CVE, verbatim from its Known Exploited Vulnerabilities catalog: the action CISA requires of US federal civilian agencies under Binding Operational Directive 22-01, by kev_due_date. It is CISA's text, not EchelonGraph's advice. " +
|
|
2300
|
+
const KEV_REQUIRED_ACTION_FIELD = "CISA's requiredAction for the CVE, verbatim from its Known Exploited Vulnerabilities catalog: the action CISA requires of US federal civilian agencies under Binding Operational Directive 26-04 (which replaced BOD 22-01 on 10 June 2026), by kev_due_date. It is CISA's text, not EchelonGraph's advice. " +
|
|
2118
2301
|
KEV_TEXT_ABSENT;
|
|
2119
2302
|
const KEV_SHORT_DESCRIPTION_FIELD = "CISA's shortDescription of the vulnerability, verbatim from its Known Exploited Vulnerabilities catalog. " + KEV_TEXT_ABSENT;
|
|
2120
2303
|
const KEV_NOTES_URLS_FIELD = "The https links in CISA's notes for the CVE, in CISA's order, de-duplicated, at most 10: the references CISA's catalog entry gives, such as the vendor's advisory or instructions and NVD's page. " +
|
|
2121
2304
|
KEV_TEXT_ABSENT;
|
|
2122
2305
|
// #2841: what get_cve's remediation is. One constant string (the site's tool-claims check folds
|
|
2123
2306
|
// only constant strings).
|
|
2124
|
-
const REMEDIATION_SUMMARY_FIELD = "A capped summary (at most 1,500 characters of JSON) of how the CVE is fixed, as its sources state it: vendor_remediation_kinds (per kind, the vendor advisories listing one for this CVE, by the vendor's own category), vendor_advisories, vendors_not_parsed, kev_required_action (CISA's, verbatim), fixed_branches_count, fixed_versions (per package, fixed versions on record), fix_references (URLs tagged Patch or Mitigation), failed_sections, truncated, and see, the name of the tool that returns the whole, with each text. A kind missing from vendor_remediation_kinds is not a finding that no such remediation exists. Absent for a REJECTED CVE and from an API older than the field.";
|
|
2307
|
+
const REMEDIATION_SUMMARY_FIELD = "A capped summary (at most 1,500 characters of JSON) of how the CVE is fixed, as its sources state it: fix_status first (the API's fix-status rule: state, basis, described_by, solution_by, stated_in_record, listed_in_patch_table and unread_inputs), vendor_remediation_kinds (per kind, the vendor advisories listing one for this CVE, by the vendor's own category), vendor_advisories, vendors_not_parsed, kev_required_action (CISA's, verbatim), fixed_branches_count, fixed_versions (per package, fixed versions on record), fix_references (URLs tagged Patch or Mitigation by NVD, or patch or mitigation by the CNA), failed_sections, truncated, and see, the name of the tool that returns the whole, with each text. A kind missing from vendor_remediation_kinds is not a finding that no such remediation exists. Absent for a REJECTED CVE and from an API older than the field.";
|
|
2125
2308
|
const CVERecord = z.looseObject({
|
|
2126
2309
|
cve_id: opt(z.string()),
|
|
2127
2310
|
description: opt(z.string()),
|
|
@@ -2136,6 +2319,9 @@ const CVERecord = z.looseObject({
|
|
|
2136
2319
|
score_confidence: opt(z.string()).describe("How confident EchelonGraph is in its score: NONE when score_assessed is false."),
|
|
2137
2320
|
score_assessed: opt(z.boolean()).describe("Whether EchelonGraph has scored the CVE. true: echelongraph_score, echelongraph_severity and echelongraph_risk are its score. false: the CVE is NOT YET SCORED, not scored 0, and any of those three it carries is a placeholder, not a rating; the API may leave them out. Absent (an API older than the field): the answer does not say whether the CVE was scored."),
|
|
2138
2321
|
score_unassessed_reason: opt(z.string()).describe("Why score_assessed is false: no_signal, no source has yet published severity data EchelonGraph can score; or rejected, the record was withdrawn by its numbering authority and is never scored. Absent when the CVE is scored."),
|
|
2322
|
+
// #2880: the SSVC decision, and CISA BOD 26-04's timeline (bodNote words it in the note).
|
|
2323
|
+
echelongraph_ssvc_decision: opt(z.string()).describe("The SSVC decision (Act, Attend, Track* or Track) EchelonGraph computes with CISA's decision table at medium mission impact. CISA publishes the table and, for some CVEs, its inputs, not a decision for a CVE. score_factors.ssvc gives the decision at low, medium and high mission impact, with each input and its source."),
|
|
2324
|
+
score_factors: z.unknown().optional().describe("EchelonGraph's evidence for its score. ssvc: the SSVC decision at every mission impact level, each input with its source. bod_26_04 (scorer v3.4 and later): CISA BOD 26-04's remediation timeline, if_publicly_exposed and if_not_publicly_exposed, each a list of CERT/CC's keys (3DF, 3D, 14D, 60D, FSU) named in timelines, with kev, automatable and technical_impact, each with its source; source cisa_bod_26_04_default is CISA's own default for a CVE not in KEV that nobody has assessed."),
|
|
2139
2325
|
epss_score: opt(z.number()),
|
|
2140
2326
|
epss_percentile: opt(z.number()),
|
|
2141
2327
|
kev_listed: opt(z.boolean()),
|
|
@@ -2149,12 +2335,18 @@ const CVERecord = z.looseObject({
|
|
|
2149
2335
|
// #2861: fix evidence. A label the API adds later is admitted, never a failed call.
|
|
2150
2336
|
patch_available: opt(z.boolean()).describe(PATCH_AVAILABLE_FIELD),
|
|
2151
2337
|
patch_evidence: opt(z.array(z.union([z.enum(PATCH_EVIDENCE_LABELS), z.string()]))).describe(PATCH_EVIDENCE_FIELD),
|
|
2338
|
+
// #3070, #3076: what the CVE record's CNA wrote (see cnaRecordNote).
|
|
2339
|
+
date_public: opt(z.string()).describe(DATE_PUBLIC_FIELD),
|
|
2340
|
+
date_public_source: opt(z.string()).describe(DATE_PUBLIC_SOURCE_FIELD),
|
|
2341
|
+
cna: opt(z.looseObject({ short_name: opt(z.string()), name: opt(z.string()) })).describe(CNA_FIELD),
|
|
2342
|
+
cna_solution: opt(z.string()).describe(CNA_SOLUTION_FIELD),
|
|
2152
2343
|
references: z.unknown().optional(),
|
|
2153
2344
|
// #2720: NVD's configurations, losslessly, beside the flat list (see cpeConfigurationsNote).
|
|
2154
2345
|
cpe_match: z.unknown().optional().describe(CPE_MATCH_DESCRIPTION),
|
|
2155
2346
|
cpe_configurations: opt(z.array(z.unknown())).describe(CPE_CONFIGURATIONS_DESCRIPTION),
|
|
2156
|
-
// #2841: a capped summary of how the CVE is fixed; cve_remediation returns the whole.
|
|
2157
|
-
|
|
2347
|
+
// #2841: a capped summary of how the CVE is fixed; cve_remediation returns the whole. Its first
|
|
2348
|
+
// field, fix_status, is the API's fix-status rule, relayed (src/fixStatus.ts).
|
|
2349
|
+
remediation: opt(z.looseObject({ fix_status: fixStatusSchema() })).describe(REMEDIATION_SUMMARY_FIELD),
|
|
2158
2350
|
published: opt(z.string()),
|
|
2159
2351
|
modified: opt(z.string()),
|
|
2160
2352
|
updated_at: opt(z.string()),
|
|
@@ -2409,9 +2601,10 @@ export function createServer() {
|
|
|
2409
2601
|
server.registerTool("cve_summary", {
|
|
2410
2602
|
title: "CVE feed summary",
|
|
2411
2603
|
description: `Summary of EchelonGraph's CVE Pulse feed: summary.total active CVEs, their counts by severity band (summary.critical, summary.high, summary.medium, summary.low), the count with no band (summary.none), and summary.last_updated, the newest modification time among those records. ${SUMMARY_NONE_DESCRIPTION} ${SUMMARY_NVD_DESCRIPTION} ${SUMMARY_REJECTED_DESCRIPTION} ${SUMMARY_POLLER_HEAD} The feed is polled from its sources on a schedule; this is the state as of that update. ${CVE_SUMMARY_ENVELOPE} ${TEXT_BUDGET_DESCRIPTION}`,
|
|
2604
|
+
inputSchema: z.object({ full: FULL_ARG }),
|
|
2412
2605
|
outputSchema: CVE_SUMMARY_OUTPUT,
|
|
2413
2606
|
annotations: ANNOTATIONS,
|
|
2414
|
-
}, async () => checked("cve_summary", CVE_SUMMARY_OUTPUT, await cveSummary()));
|
|
2607
|
+
}, async (a) => checked("cve_summary", CVE_SUMMARY_OUTPUT, await answering(a.full, NO_DATA_CUT, () => cveSummary())));
|
|
2415
2608
|
server.registerTool("search_cves", {
|
|
2416
2609
|
title: "Search CVEs",
|
|
2417
2610
|
description: `Search/list CVEs from EchelonGraph's CVE feed (NVD + MITRE-CNA pre-NVD + CISA-KEV + EPSS + GitHub GHSA, each polled on a schedule). Filter by severity, minimum CVSS, free text, and sort; page with limit and offset. Returns cves, each with cve_id, severity, cvss_v3_score, echelongraph_score and score_assessed (whether EchelonGraph has scored it), epss_score and kev_listed where the record has them, and the list's total, total_counted (false: total is not a count), total_is_lower_bound (true: at least total), search_relaxed, limit and offset. ${SCORE_ASSESSED_DESCRIPTION} ${PATCH_AVAILABLE_DESCRIPTION} ${FEED_ENVELOPE} ${TEXT_BUDGET_DESCRIPTION} Cut, each row keeps at least the fields named above and the first 200 characters of its description (100 on a page too long for that), or rows are left out and the note gives the offset to call next.`,
|
|
@@ -2423,30 +2616,32 @@ export function createServer() {
|
|
|
2423
2616
|
limit: z.number().int().min(1).max(50).optional().describe("page size (default 20, max 50)"),
|
|
2424
2617
|
// #2783: the next page, which the note names when the text leaves rows out.
|
|
2425
2618
|
offset: z.number().int().min(0).max(10000).optional().describe("rows to skip (default 0)"),
|
|
2619
|
+
full: FULL_ARG,
|
|
2426
2620
|
}),
|
|
2427
2621
|
outputSchema: SEARCH_CVES_OUTPUT,
|
|
2428
2622
|
annotations: ANNOTATIONS,
|
|
2429
|
-
}, async (a) => checked("search_cves", SEARCH_CVES_OUTPUT, await searchCVEs(a)));
|
|
2623
|
+
}, async (a) => checked("search_cves", SEARCH_CVES_OUTPUT, await answering(a.full, SEARCH_CVES_DATA, () => searchCVEs(a))));
|
|
2430
2624
|
server.registerTool("get_cve", {
|
|
2431
2625
|
title: "CVE detail",
|
|
2432
2626
|
description: `One CVE's record: description, severity, cvss_v3_score, cvss_v4_score and cvss_v4_severity, echelongraph_score, echelongraph_severity and score_confidence, epss_score and epss_percentile, CISA-KEV status (kev_listed, kev_added_date, kev_due_date, kev_ransomware) and, if KEV-listed, CISA's own text relayed verbatim (kev_required_action, not EchelonGraph's advice; kev_short_description; kev_notes_urls), ghsa_id, references, cpe_match (one flat list) and cpe_configurations (NVD's configurations as NVD sent them, with each AND/OR operator; absent where none is stored, which is not a finding that no product is affected), remediation (a capped summary of its fix), published, modified and updated_at; each field only where the record has it. Pass a CVE ID like CVE-2023-44487. ${SCORE_ASSESSED_DESCRIPTION} ${PATCH_AVAILABLE_DESCRIPTION} ${FEED_ENVELOPE} ${TEXT_BUDGET_DESCRIPTION} Cut, each list in the record keeps its first entries, and the note names each list cut with its full length.`,
|
|
2433
|
-
inputSchema: z.object({ cve_id: CVE_ID_ARG }),
|
|
2627
|
+
inputSchema: z.object({ cve_id: CVE_ID_ARG, full: FULL_ARG }),
|
|
2434
2628
|
outputSchema: GET_CVE_OUTPUT,
|
|
2435
2629
|
annotations: ANNOTATIONS,
|
|
2436
|
-
}, async ({ cve_id }) => checked("get_cve", GET_CVE_OUTPUT, await getCVE(cve_id)));
|
|
2630
|
+
}, async ({ cve_id, full }) => checked("get_cve", GET_CVE_OUTPUT, await answering(full, GET_CVE_DATA, () => getCVE(cve_id))));
|
|
2437
2631
|
server.registerTool("cve_exposure", {
|
|
2438
2632
|
title: "Internet exposure for one CVE",
|
|
2439
2633
|
description: `${CVE_EXPOSURE_DESCRIPTION} Its structured result's state is not_assessed with measured_at null, since the answer dates no observation; the count is still relayed, labelled, as what the radar holds on record. exposure_state says what the count is: exposed, measured_zero, not_assessed or tracking_unknown; coverage.in_scope is the API's tracked verdict; freshness is null; data is the API's JSON. ${TEXT_BUDGET_DESCRIPTION}`,
|
|
2440
|
-
inputSchema: z.object({ cve_id: CVE_ID_ARG }),
|
|
2634
|
+
inputSchema: z.object({ cve_id: CVE_ID_ARG, full: FULL_ARG }),
|
|
2441
2635
|
outputSchema: CVE_EXPOSURE_OUTPUT,
|
|
2442
2636
|
annotations: ANNOTATIONS,
|
|
2443
|
-
}, async ({ cve_id }) => checked("cve_exposure", CVE_EXPOSURE_OUTPUT, await cveExposure(cve_id)));
|
|
2637
|
+
}, async ({ cve_id, full }) => checked("cve_exposure", CVE_EXPOSURE_OUTPUT, await answering(full, NO_DATA_CUT, () => cveExposure(cve_id))));
|
|
2444
2638
|
server.registerTool("exposure_radar", {
|
|
2445
2639
|
title: "Exposure radar totals",
|
|
2446
2640
|
description: `${EXPOSURE_RADAR_DESCRIPTION} Its structured result's state is not_assessed with measured_at null, since no radar dates what it counts; each count is still relayed as what that radar holds on record. freshness gives each radar's last_run_at where the API serves one; coverage names the radars that answered. ${TEXT_BUDGET_DESCRIPTION}`,
|
|
2641
|
+
inputSchema: z.object({ full: FULL_ARG }),
|
|
2447
2642
|
outputSchema: EXPOSURE_RADAR_OUTPUT,
|
|
2448
2643
|
annotations: ANNOTATIONS,
|
|
2449
|
-
}, async () => checked("exposure_radar", EXPOSURE_RADAR_OUTPUT, await exposureRadar()));
|
|
2644
|
+
}, async (a) => checked("exposure_radar", EXPOSURE_RADAR_OUTPUT, await answering(a.full, EXPOSURE_RADAR_DATA, () => exposureRadar())));
|
|
2450
2645
|
registerKevRecent(server, { api, succeeded, failed, badInput, crashed, checked, envelopeSchema, okHead, annotations: ANNOTATIONS });
|
|
2451
2646
|
registerEPSSHistory(server, TOOL_KIT);
|
|
2452
2647
|
// #2716: its logic, schemas and description live in tools/check_affected.ts.
|
|
@@ -2467,7 +2662,8 @@ export function createServer() {
|
|
|
2467
2662
|
// #2722: prompts and resources, in prompts.ts and resources.ts. Both lists are fixed per
|
|
2468
2663
|
// connection, so listChanged is false above.
|
|
2469
2664
|
registerPrompts(server, { cveId: CVE_ID });
|
|
2470
|
-
|
|
2665
|
+
// #3013: cve:// cuts get_cve's data itself (DATA CUT, resources.ts), so it reads it whole.
|
|
2666
|
+
registerResources(server, { api, cveId: CVE_ID, shownBase: SHOWN_BASE, sentences, getCve: async (id) => checked("get_cve", GET_CVE_OUTPUT, await answering(true, GET_CVE_DATA, () => getCVE(id))) });
|
|
2471
2667
|
return server;
|
|
2472
2668
|
}
|
|
2473
2669
|
// The package identity and API settings, for the hosted HTTP entrypoint's health answer and
|