mcp-app-lint 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/analyze.d.mts +1 -0
- package/dist/analyze.d.mts.map +1 -1
- package/dist/lint.d.mts +4 -1
- package/dist/lint.d.mts.map +1 -1
- package/package.json +1 -1
- package/src/analyze.mjs +22 -11
- package/src/lint.mjs +28 -6
- package/src/rules.mjs +1 -1
- package/src/sarif.mjs +1 -1
package/README.md
CHANGED
|
@@ -54,7 +54,7 @@ npx mcp-app-lint --dir . --format sarif --out mcp-app-lint.sarif --fail-on error
|
|
|
54
54
|
|
|
55
55
|
| Id | Name | Level | What it means |
|
|
56
56
|
| --- | --- | --- | --- |
|
|
57
|
-
| MCPAPP001 | csp-missing | warning | no `ui.csp
|
|
57
|
+
| MCPAPP001 | csp-missing | warning | no `ui.csp`, but the UI loads from or calls an external host (an attribute such as `src`/`href`, or an absolute URL in `fetch`, `WebSocket`, `EventSource`, XHR, `sendBeacon`, `import()`); a compliant host applies `default-src 'none'`, `connect-src 'none'` and blocks it. A self-contained UI needs no CSP and is not reported. For a package: it serves a UI resource, no domain list is found (literal or built at runtime), and one of its HTML documents references a host |
|
|
58
58
|
| MCPAPP002 | csp-allows-every-host | error | `*` or a scheme-only entry (`https:`) expands to every host |
|
|
59
59
|
| MCPAPP003 | csp-wildcard-host | warning | `*.example.com` allows every subdomain |
|
|
60
60
|
| MCPAPP004 | csp-insecure-scheme | warning | `http:` origin other than loopback |
|
|
@@ -82,7 +82,9 @@ In a package scan, domain lists are read where they are written: as a literal ar
|
|
|
82
82
|
constant or shorthand defined in the same file (`resourceDomains: DOMAINS`, `{ connectDomains }`).
|
|
83
83
|
Each CSP finding points at that file and line. A list built at runtime counts as declared, so it
|
|
84
84
|
does not raise MCPAPP001, but its entries cannot be checked. Packages that only mention the MCP
|
|
85
|
-
Apps MIME type or `ui://` (hosts, renderers, SDKs) are not treated as serving a UI resource.
|
|
85
|
+
Apps MIME type or `ui://` (hosts, renderers, SDKs) are not treated as serving a UI resource. Hosts are
|
|
86
|
+
looked for in the package's HTML documents only: a `fetch` in server code is not bound by the CSP,
|
|
87
|
+
so it does not count.
|
|
86
88
|
|
|
87
89
|
The rules come out of the [MCP Apps UI census](../../census) in this repository; the analyzer is
|
|
88
90
|
shared. The HTML sink detection is `eslint-plugin-render-policy`'s `no-unsafe-innerhtml`. The sink
|
package/dist/analyze.d.mts
CHANGED
package/dist/analyze.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyze.d.mts","sourceRoot":"","sources":["../src/analyze.mjs"],"names":[],"mappings":"AAgBG,YAA+B,MAAM,GAA3B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAQ;AAEpC,YAAoF,MAAM,GAAhF,gBAAgB,GAAG,iBAAiB,GAAG,cAAc,GAAG,gBAAgB,CAAQ;AAK9F,eAAO,MAAM,OAAO,8BAA8B,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAHlB,OAGkB,GAFhB,MAAM,GAAG,IAAI,CAWzB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAFvB,OAEuB;;;;;;;;;;;;;;;;;;;;;;EAoBjC;AAeD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAH7B,OAG6B,EAAE,QAAQ,EAFvC,OAEuC;;;;;;;;;;;;EAajD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAFvB,OAEuB;;;;;;EAkBjC;AAED,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB,UAA0G,CAAC;AAyDrI,YAA+D,eAAe,GAApE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAiB;AAI9E,YAAkB,QAAQ,GAC1B;IAAmB,IAAI,EAAZ,MAAM,CACjB;IAAmB,MAAM,EAAd,MAAM,CACjB;;;;IAAoB,WAAW,EAApB,OAAO,CAClB;;;;IAAoB,OAAO,EAAhB,OAAO,CAClB;IAAmB,OAAO,EAAf,MAAM,CACnB;CAAA,CAAA;AAZD;;;GAGG;AACH;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAH9B,MAG8B,GAF5B,eAAe,EAAE,CAc7B;
|
|
1
|
+
{"version":3,"file":"analyze.d.mts","sourceRoot":"","sources":["../src/analyze.mjs"],"names":[],"mappings":"AAgBG,YAA+B,MAAM,GAA3B,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAQ;AAEpC,YAAoF,MAAM,GAAhF,gBAAgB,GAAG,iBAAiB,GAAG,cAAc,GAAG,gBAAgB,CAAQ;AAK9F,eAAO,MAAM,OAAO,8BAA8B,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAHlB,OAGkB,GAFhB,MAAM,GAAG,IAAI,CAWzB;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAFvB,OAEuB;;;;;;;;;;;;;;;;;;;;;;EAoBjC;AAeD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAH7B,OAG6B,EAAE,QAAQ,EAFvC,OAEuC;;;;;;;;;;;;EAajD;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAFvB,OAEuB;;;;;;EAkBjC;AAED,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB,UAA0G,CAAC;AAyDrI,YAA+D,eAAe,GAApE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAiB;AAI9E,YAAkB,QAAQ,GAC1B;IAAmB,IAAI,EAAZ,MAAM,CACjB;IAAmB,MAAM,EAAd,MAAM,CACjB;;;;IAAoB,WAAW,EAApB,OAAO,CAClB;;;;IAAoB,OAAO,EAAhB,OAAO,CAClB;IAAmB,OAAO,EAAf,MAAM,CACnB;CAAA,CAAA;AAZD;;;GAGG;AACH;;;;;;;;GAQG;AAEH;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAH9B,MAG8B,GAF5B,eAAe,EAAE,CAc7B;AAqBD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAFrB,OAEqB;;;;qBAMA,MAAM,EAAE;;;;;;;kBAOX,KAAK,CAAC,eAAe,GAAG;QAAE,WAAW,EAAE,OAAO,CAAA;KAAE,CAAC;;;;WAIxD,QAAQ,EAAE;;;;;;mBAMF,MAAM,EAAE;kBAET,MAAM,EAAE;eACX,MAAM,EAAE;EAiDjC;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAFvB,MAEuB,WAKjC;AAEG,YAA0C,YAAY,GAA5C,UAAU,CAAC,OAAO,WAAW,CAAC,CAAc;AAA1D,6DAA6D;AAE7D;;;;;GAKG;AACH,wBAAgB,aAAa,CAJc,CAAC,SAA9B;IAAE,aAAa,EAAE,MAAM,EAAE,CAAA;CAAK,EAId,QAAQ,EAH3B,CAG2B,GAFzB,CAAC,GAAG;IAAE,SAAS,EAAE,MAAM,EAAE,CAAA;CAAE,CAIvC"}
|
package/dist/lint.d.mts
CHANGED
|
@@ -31,10 +31,13 @@ export type ResourceInput = {
|
|
|
31
31
|
};
|
|
32
32
|
/**
|
|
33
33
|
* Findings about one `_meta` object of a UI resource (the resolved one: read, falling back to list).
|
|
34
|
+
* `hosts` are the hosts the UI references (see lintResource); without a CSP the host blocks them,
|
|
35
|
+
* and that is MCPAPP001. A UI that references none needs no CSP: the restrictive default fits it.
|
|
34
36
|
* @param {unknown} meta
|
|
35
37
|
* @param {string} uri
|
|
38
|
+
* @param {readonly string[]} [hosts]
|
|
36
39
|
*/
|
|
37
|
-
export declare function lintUiMeta(meta: unknown, uri: string): Finding[];
|
|
40
|
+
export declare function lintUiMeta(meta: unknown, uri: string, hosts?: readonly string[]): Finding[];
|
|
38
41
|
/**
|
|
39
42
|
* Findings about a UI resource as served: listing entry, read result content item, HTML.
|
|
40
43
|
* @param {ResourceInput} resource
|
package/dist/lint.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lint.d.mts","sourceRoot":"","sources":["../src/lint.mjs"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAe,YAAY,EAA2D,MAAM,eAAe,CAAC;AAMhH,OAAQ,KAAA,EAAE,WAAW,EAAE,MAAM,WAAW,CACxC;AAAA,OAAQ,KAAA,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAMpC,YAAkB,OAAO,GACzB;IAAmB,MAAM,EAAd,MAAM,CACjB;;;;IAAmB,KAAK,AAAxB,CACA,EADW,KAAK,CAChB;IAAmB,OAAO,EAAf,MAAM,CACjB;IAAmB,GAAG,EAAX,MAAM,CACjB;IAAyD,MAAM,AAA/D,CACA,EADW;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CACtD;IAA4C,OAAO,AAAnD,CACA,EADW;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CACzC;IAAqC,UAAU,AAA/C,CAAgD,EAArC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAc;CAAA,CAAA;AAE/C,YAAuD,YAAY,GAAzD,IAAI,CAAC,OAAO,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC,CAAc;AAIpE,YAAkB,aAAa,GAC/B;IAAmB,GAAG,EAAX,MAAM,CACjB;IAAqB,QAAQ,AAA7B,CACA,EADW,OAAO,CAClB;IAAqB,QAAQ,AAA7B,CACA,EADW,OAAO,CAClB;IAA2B,IAAI,AAA/B,CAAgC,EAArB,MAAM,GAAG,IAAI,CAAQ;CAAA,CAAA;
|
|
1
|
+
{"version":3,"file":"lint.d.mts","sourceRoot":"","sources":["../src/lint.mjs"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAe,YAAY,EAA2D,MAAM,eAAe,CAAC;AAMhH,OAAQ,KAAA,EAAE,WAAW,EAAE,MAAM,WAAW,CACxC;AAAA,OAAQ,KAAA,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAMpC,YAAkB,OAAO,GACzB;IAAmB,MAAM,EAAd,MAAM,CACjB;;;;IAAmB,KAAK,AAAxB,CACA,EADW,KAAK,CAChB;IAAmB,OAAO,EAAf,MAAM,CACjB;IAAmB,GAAG,EAAX,MAAM,CACjB;IAAyD,MAAM,AAA/D,CACA,EADW;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CACtD;IAA4C,OAAO,AAAnD,CACA,EADW;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CACzC;IAAqC,UAAU,AAA/C,CAAgD,EAArC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAc;CAAA,CAAA;AAE/C,YAAuD,YAAY,GAAzD,IAAI,CAAC,OAAO,EAAE,QAAQ,GAAG,SAAS,GAAG,KAAK,CAAC,CAAc;AAIpE,YAAkB,aAAa,GAC/B;IAAmB,GAAG,EAAX,MAAM,CACjB;IAAqB,QAAQ,AAA7B,CACA,EADW,OAAO,CAClB;IAAqB,QAAQ,AAA7B,CACA,EADW,OAAO,CAClB;IAA2B,IAAI,AAA/B,CAAgC,EAArB,MAAM,GAAG,IAAI,CAAQ;CAAA,CAAA;AAgDnC;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAJpB,OAIoB,EAAE,GAAG,EAHzB,MAGyB,EAAE,KAAK,AAFxC,CAAkC,EAA1B,SAAS,MAAM,EAEsB,aAgB/C;AASD;;;GAGG;AACH,wBAAgB,YAAY,CAAC,EAAE,GAAG,EAAE,QAAe,EAAE,QAAe,EAAE,IAAW,EAAE,EAFxE,aAEwE,aAalF;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAHlB,MAGkB,EAAE,GAAG,EAFvB,MAEuB,aAEjC;AAiCD;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAHpB,OAGoB,EAAE,GAAG,AAFjC,CAAqB,EAAb,MAEwC,aAelD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAHzB,WAGyB,EAAE,IAAI,AAFvC,CAAsB,EAAd,MAEqC,aAsB/C;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAHnB,MAGmB,GAFjB,KAAK,CAIjB;AAED,OAAO,EAAE,YAAY,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-app-lint",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "SARIF findings about what an MCP App declares (CSP domain lists, permissions, tool visibility) and what its HTML does (innerHTML sinks, postMessage to '*', external hosts, data sinks).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/analyze.mjs
CHANGED
|
@@ -211,6 +211,24 @@ export function postMessageStarCalls(code) {
|
|
|
211
211
|
return calls;
|
|
212
212
|
}
|
|
213
213
|
const URL_ATTR_RE = /\b(?:src|href|action|poster|data)\s*=\s*["']([^"']+)["']/gi;
|
|
214
|
+
// A network call with an absolute URL literal: fetch, sendBeacon, import(), WebSocket, EventSource, XHR open.
|
|
215
|
+
const NETWORK_CALL_RE = /(?:\b(?:fetch|sendBeacon|import)\s*\(|\bnew\s+(?:WebSocket|EventSource)\s*\(|\.open\s*\(\s*["'`][A-Za-z]+["'`]\s*,)\s*["'`]((?:https?|wss?):\/\/[^"'`\s]+)/g;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Hostnames of URLs, skipping the ones that do not parse.
|
|
219
|
+
* @param {Iterable<string>} urls
|
|
220
|
+
*/
|
|
221
|
+
function hostnames(urls) {
|
|
222
|
+
const hosts = new Set();
|
|
223
|
+
for (const url of urls) {
|
|
224
|
+
try {
|
|
225
|
+
hosts.add(new URL(url.startsWith('//') ? `https:${url}` : url).hostname.toLowerCase());
|
|
226
|
+
} catch {
|
|
227
|
+
/* ignore */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return [...hosts].sort();
|
|
231
|
+
}
|
|
214
232
|
|
|
215
233
|
/**
|
|
216
234
|
* Static signals in a UI resource's HTML. Heuristic by design; the report says so.
|
|
@@ -240,19 +258,12 @@ export function analyzeHtml(html) {
|
|
|
240
258
|
formsWithAction: (text.match(/<form\b[^>]*\baction\s*=/gi) ?? []).length,
|
|
241
259
|
metaCsp: /<meta\b[^>]*http-equiv\s*=\s*["']content-security-policy["']/i.test(text),
|
|
242
260
|
externalHosts: /** @type {string[]} */ ([]),
|
|
261
|
+
// Hosts that script calls with an absolute URL (fetch, WebSocket, …), bundles included.
|
|
262
|
+
networkHosts: /** @type {string[]} */ ([]),
|
|
243
263
|
sinkHosts: /** @type {string[]} */ ([]),
|
|
244
264
|
};
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const value = match[1] ?? '';
|
|
248
|
-
if (!/^(https?:)?\/\//i.test(value)) continue;
|
|
249
|
-
try {
|
|
250
|
-
hosts.add(new URL(value.startsWith('//') ? `https:${value}` : value).hostname.toLowerCase());
|
|
251
|
-
} catch {
|
|
252
|
-
/* ignore */
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
result.externalHosts = [...hosts].sort();
|
|
265
|
+
result.externalHosts = hostnames([...text.matchAll(URL_ATTR_RE)].map((m) => m[1] ?? '').filter((v) => /^(https?:)?\/\//i.test(v)));
|
|
266
|
+
result.networkHosts = hostnames([...text.matchAll(NETWORK_CALL_RE)].map((m) => m[1] ?? ''));
|
|
256
267
|
for (const match of text.matchAll(SCRIPT_RE)) {
|
|
257
268
|
const attrs = match[1] ?? '';
|
|
258
269
|
const body = match[2] ?? '';
|
package/src/lint.mjs
CHANGED
|
@@ -69,18 +69,31 @@ function cspEntryFindings(key, raw, uri, extra) {
|
|
|
69
69
|
return findings;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/**
|
|
73
|
+
* The hosts a UI document loads from or calls, sorted and without duplicates.
|
|
74
|
+
* @param {Iterable<{ externalHosts?: string[], networkHosts?: string[] }>} docs
|
|
75
|
+
*/
|
|
76
|
+
function referencedHosts(docs) {
|
|
77
|
+
const hosts = new Set();
|
|
78
|
+
for (const doc of docs) for (const host of [...(doc.externalHosts ?? []), ...(doc.networkHosts ?? [])]) hosts.add(host);
|
|
79
|
+
return [...hosts].sort();
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
/**
|
|
73
83
|
* Findings about one `_meta` object of a UI resource (the resolved one: read, falling back to list).
|
|
84
|
+
* `hosts` are the hosts the UI references (see lintResource); without a CSP the host blocks them,
|
|
85
|
+
* and that is MCPAPP001. A UI that references none needs no CSP: the restrictive default fits it.
|
|
74
86
|
* @param {unknown} meta
|
|
75
87
|
* @param {string} uri
|
|
88
|
+
* @param {readonly string[]} [hosts]
|
|
76
89
|
*/
|
|
77
|
-
export function lintUiMeta(meta, uri) {
|
|
90
|
+
export function lintUiMeta(meta, uri, hosts = []) {
|
|
78
91
|
/** @type {Finding[]} */
|
|
79
92
|
const findings = [];
|
|
80
93
|
const ui = uiMetaOf(meta);
|
|
81
94
|
const analysis = analyzeUiMeta(meta);
|
|
82
95
|
if (!analysis.cspDeclared) {
|
|
83
|
-
|
|
96
|
+
if (hosts.length > 0) findings.push(cspMissing(hosts, uri));
|
|
84
97
|
} else {
|
|
85
98
|
for (const key of CSP_KEYS) {
|
|
86
99
|
for (const raw of ui?.csp?.[key] ?? []) findings.push(...cspEntryFindings(key, raw, uri, { logical: { name: `csp.${key}`, kind: 'member' } }));
|
|
@@ -92,6 +105,13 @@ export function lintUiMeta(meta, uri) {
|
|
|
92
105
|
return findings;
|
|
93
106
|
}
|
|
94
107
|
|
|
108
|
+
/**
|
|
109
|
+
* @param {readonly string[]} hosts
|
|
110
|
+
* @param {string} uri
|
|
111
|
+
*/
|
|
112
|
+
const cspMissing = (hosts, uri) =>
|
|
113
|
+
finding('MCPAPP001', `No ui.csp declared, but the UI references ${hosts.join(', ')}; a compliant host applies default-src 'none' and blocks them.`, uri, { properties: { hosts: [...hosts] } });
|
|
114
|
+
|
|
95
115
|
/**
|
|
96
116
|
* Findings about a UI resource as served: listing entry, read result content item, HTML.
|
|
97
117
|
* @param {ResourceInput} resource
|
|
@@ -100,13 +120,14 @@ export function lintResource({ uri, listMeta = null, readMeta = null, html = nul
|
|
|
100
120
|
/** @type {Finding[]} */
|
|
101
121
|
const findings = [];
|
|
102
122
|
const effective = readMeta ?? listMeta;
|
|
103
|
-
|
|
123
|
+
const analysis = typeof html === 'string' ? withSinkHosts(analyzeHtml(html)) : null;
|
|
124
|
+
findings.push(...lintUiMeta(effective, uri, analysis ? referencedHosts([analysis]) : []));
|
|
104
125
|
if (listMeta && readMeta) {
|
|
105
126
|
const compared = compareListRead(listMeta, readMeta);
|
|
106
127
|
if (compared.readWider) findings.push(finding('MCPAPP018', 'resources/read declares a wider policy than resources/list.', uri));
|
|
107
128
|
else if (compared.mismatch) findings.push(finding('MCPAPP017', 'resources/read and resources/list declare different policies.', uri));
|
|
108
129
|
}
|
|
109
|
-
if (
|
|
130
|
+
if (analysis) findings.push(...htmlFindings(analysis, uri));
|
|
110
131
|
return findings;
|
|
111
132
|
}
|
|
112
133
|
|
|
@@ -187,11 +208,12 @@ export function lintPackageScan(scan, root = '.') {
|
|
|
187
208
|
if (!scan.cspDeclared) delete fake.ui.csp;
|
|
188
209
|
if (Array.isArray(scan.domainSites)) {
|
|
189
210
|
// Each entry where it is written, so code scanning can point at the line.
|
|
190
|
-
|
|
211
|
+
const hosts = referencedHosts(scan.html ?? []);
|
|
212
|
+
if (!scan.cspDeclared && servesUi && hosts.length > 0) findings.push(cspMissing(hosts, root));
|
|
191
213
|
for (const site of scan.domainSites) findings.push(...cspEntryFindings(site.key, site.raw, `${root}/${site.file}`.replace(/^\.\//, ''), { region: { startLine: site.line } }));
|
|
192
214
|
findings.push(...lintUiMeta({ ui: { csp: {}, permissions: fake.ui.permissions } }, root).filter((f) => f.ruleId === 'MCPAPP007'));
|
|
193
215
|
} else {
|
|
194
|
-
findings.push(...lintUiMeta(fake, root));
|
|
216
|
+
findings.push(...lintUiMeta(fake, root, servesUi ? referencedHosts(scan.html ?? []) : []));
|
|
195
217
|
}
|
|
196
218
|
}
|
|
197
219
|
for (const doc of scan.html ?? []) {
|
package/src/rules.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* @type {readonly Rule[]}
|
|
10
10
|
*/
|
|
11
11
|
export const RULES = [
|
|
12
|
-
{ id: 'MCPAPP001', name: 'csp-missing', level: 'warning', short: 'UI
|
|
12
|
+
{ id: 'MCPAPP001', name: 'csp-missing', level: 'warning', short: 'UI references external hosts but declares no CSP domain list', full: "A compliant host applies default-src 'none' and connect-src 'none' to a resource without ui.csp, so the hosts the UI loads from or calls are blocked. Reported when the HTML references an external host (src, href and similar attributes) or calls one with an absolute URL (fetch, WebSocket, EventSource, XMLHttpRequest, sendBeacon, import()); a UI that references none needs no CSP. For a package, reported when it serves a UI resource (registers one, or sets the MCP Apps MIME type or a ui:// URI on a resource), no domain list is found, literal or built at runtime, and one of its HTML documents references a host." },
|
|
13
13
|
{ id: 'MCPAPP002', name: 'csp-allows-every-host', level: 'error', short: 'CSP entry allows every host', full: "A bare '*' or a scheme-only entry such as 'https:' is expanded by hosts into a CSP source that allows every host, including request catchers and tunnels. Name the hosts." },
|
|
14
14
|
{ id: 'MCPAPP003', name: 'csp-wildcard-host', level: 'warning', short: 'CSP entry allows every subdomain', full: 'A *.example.com entry allows every subdomain, including user-controlled ones on multi-tenant services. Prefer exact hosts.' },
|
|
15
15
|
{ id: 'MCPAPP004', name: 'csp-insecure-scheme', level: 'warning', short: 'CSP entry uses http:', full: 'An http: origin can be read and modified in transit. Use https:. Loopback origins are reported as development origins (MCPAPP006) instead.' },
|
package/src/sarif.mjs
CHANGED
|
@@ -68,6 +68,6 @@ export function toSarif(findings, { invocation = {} } = {}) {
|
|
|
68
68
|
export function toText(findings) {
|
|
69
69
|
if (findings.length === 0) return 'no findings\n';
|
|
70
70
|
return `${findings
|
|
71
|
-
.map((f) => `${(f.level ?? levelOf(f.ruleId)).padEnd(7)} ${f.ruleId} ${f.uri}${f.region ? `:${f.region.startLine}
|
|
71
|
+
.map((f) => `${(f.level ?? levelOf(f.ruleId)).padEnd(7)} ${f.ruleId} ${f.uri}${f.region ? `:${f.region.startLine}${f.region.startColumn ? `:${f.region.startColumn}` : ''}` : ''} ${f.message}`)
|
|
72
72
|
.join('\n')}\n`;
|
|
73
73
|
}
|