mcp-medic 1.2.7 → 1.2.8
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/dist/fleet.js +96 -7
- package/dist/redact.js +1 -1
- package/package.json +1 -1
package/dist/fleet.js
CHANGED
|
@@ -3,7 +3,7 @@ import { resolve, join } from 'node:path';
|
|
|
3
3
|
import { loadConfig } from './config-loader.js';
|
|
4
4
|
import { runChecks } from './orchestrator.js';
|
|
5
5
|
import { allChecks } from './checks/index.js';
|
|
6
|
-
import { redactRecord } from './redact.js';
|
|
6
|
+
import { isSecretKey, redactDeep, redactRecord } from './redact.js';
|
|
7
7
|
async function mapWithConcurrency(items, concurrency, worker) {
|
|
8
8
|
const results = new Array(items.length);
|
|
9
9
|
let nextIndex = 0;
|
|
@@ -18,6 +18,74 @@ async function mapWithConcurrency(items, concurrency, worker) {
|
|
|
18
18
|
}));
|
|
19
19
|
return results;
|
|
20
20
|
}
|
|
21
|
+
function canonicalize(value) {
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
return value.map(canonicalize);
|
|
24
|
+
if (value && typeof value === 'object') {
|
|
25
|
+
return Object.fromEntries(Object.keys(value)
|
|
26
|
+
.sort()
|
|
27
|
+
.map((key) => [key, canonicalize(value[key])]));
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function stableStringify(value) {
|
|
32
|
+
return JSON.stringify(canonicalize(value)) ?? 'undefined';
|
|
33
|
+
}
|
|
34
|
+
function redactInlineSecrets(value) {
|
|
35
|
+
return value
|
|
36
|
+
.replace(/(\bBearer\s+)[^\s,;"']+/gi, '$1[REDACTED]')
|
|
37
|
+
.replace(/([a-z0-9_.-]*(?:authorization|token|secret|password|passwd|pwd|api[-_]?key|apikey|cookie|credential|bearer)[a-z0-9_.-]*\s*[:=]\s*)("[^"]*"|'[^']*'|[^\s,;]+)/gi, '$1[REDACTED]');
|
|
38
|
+
}
|
|
39
|
+
function redactArgs(args) {
|
|
40
|
+
if (!args)
|
|
41
|
+
return args;
|
|
42
|
+
const result = [...args];
|
|
43
|
+
for (let i = 0; i < result.length; i += 1) {
|
|
44
|
+
const arg = result[i];
|
|
45
|
+
const equalsIndex = arg.indexOf('=');
|
|
46
|
+
if (equalsIndex >= 0 && isSecretKey(arg.slice(0, equalsIndex).replace(/^-+/, ''))) {
|
|
47
|
+
result[i] = `${arg.slice(0, equalsIndex + 1)}[REDACTED]`;
|
|
48
|
+
}
|
|
49
|
+
else if (isSecretKey(arg.replace(/^-+/, '')) && i + 1 < result.length) {
|
|
50
|
+
result[i + 1] = '[REDACTED]';
|
|
51
|
+
i += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result.map(redactInlineSecrets);
|
|
55
|
+
}
|
|
56
|
+
/** Removes URL credentials and secret-looking query/fragment values before a diff is displayed. */
|
|
57
|
+
function redactUrl(value) {
|
|
58
|
+
if (value === undefined)
|
|
59
|
+
return undefined;
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(value);
|
|
62
|
+
if (url.username)
|
|
63
|
+
url.username = '[REDACTED]';
|
|
64
|
+
if (url.password)
|
|
65
|
+
url.password = '[REDACTED]';
|
|
66
|
+
for (const key of [...url.searchParams.keys()]) {
|
|
67
|
+
if (isSecretKey(key))
|
|
68
|
+
url.searchParams.set(key, '[REDACTED]');
|
|
69
|
+
}
|
|
70
|
+
if (url.hash)
|
|
71
|
+
url.hash = '[REDACTED]';
|
|
72
|
+
return url.toString();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Invalid URL-like values still get a conservative pass over common credential forms.
|
|
76
|
+
return value
|
|
77
|
+
.replace(/([a-z][a-z0-9+.-]*:\/\/)[^/@?#]+@/i, '$1[REDACTED]@')
|
|
78
|
+
.replace(/([?&])([^=&#]+)=([^&#]*)/g, (match, separator, rawKey) => {
|
|
79
|
+
let key = rawKey;
|
|
80
|
+
try {
|
|
81
|
+
key = decodeURIComponent(rawKey.replace(/\+/g, ' '));
|
|
82
|
+
}
|
|
83
|
+
catch { /* retain raw key */ }
|
|
84
|
+
return isSecretKey(key) ? `${separator}${rawKey}=[REDACTED]` : match;
|
|
85
|
+
})
|
|
86
|
+
.replace(/#.*$/, '#[REDACTED]');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
21
89
|
function findFilesMatching(dir, rootDir, pattern, results = []) {
|
|
22
90
|
if (!existsSync(dir))
|
|
23
91
|
return results;
|
|
@@ -145,20 +213,41 @@ export function diffConfigs(configA, configB) {
|
|
|
145
213
|
changes.push({ field: 'transport', from: serverA.transport, to: serverB.transport });
|
|
146
214
|
}
|
|
147
215
|
if (serverA.command !== serverB.command) {
|
|
148
|
-
changes.push({
|
|
216
|
+
changes.push({
|
|
217
|
+
field: 'command',
|
|
218
|
+
from: serverA.command === undefined ? undefined : redactInlineSecrets(serverA.command),
|
|
219
|
+
to: serverB.command === undefined ? undefined : redactInlineSecrets(serverB.command),
|
|
220
|
+
});
|
|
149
221
|
}
|
|
150
|
-
if (
|
|
151
|
-
changes.push({ field: 'args', from: serverA.args, to: serverB.args });
|
|
222
|
+
if (stableStringify(serverA.args) !== stableStringify(serverB.args)) {
|
|
223
|
+
changes.push({ field: 'args', from: redactArgs(serverA.args), to: redactArgs(serverB.args) });
|
|
152
224
|
}
|
|
153
|
-
if (serverA.url !== serverB.url) {
|
|
154
|
-
changes.push({ field: 'url', from: serverA.url, to: serverB.url });
|
|
225
|
+
if (stableStringify(serverA.url) !== stableStringify(serverB.url)) {
|
|
226
|
+
changes.push({ field: 'url', from: redactUrl(serverA.url), to: redactUrl(serverB.url) });
|
|
155
227
|
}
|
|
156
|
-
if (
|
|
228
|
+
if (stableStringify(serverA.env) !== stableStringify(serverB.env)) {
|
|
157
229
|
// Report that env changed and which keys, but never raw values —
|
|
158
230
|
// env vars routinely carry API keys/tokens and diff output gets
|
|
159
231
|
// pasted into PRs and CI logs.
|
|
160
232
|
changes.push({ field: 'env', from: redactRecord(serverA.env), to: redactRecord(serverB.env) });
|
|
161
233
|
}
|
|
234
|
+
if (stableStringify(serverA.headers) !== stableStringify(serverB.headers)) {
|
|
235
|
+
changes.push({ field: 'headers', from: redactRecord(serverA.headers), to: redactRecord(serverB.headers) });
|
|
236
|
+
}
|
|
237
|
+
if (stableStringify(serverA.tokenRefreshUrl) !== stableStringify(serverB.tokenRefreshUrl)) {
|
|
238
|
+
changes.push({
|
|
239
|
+
field: 'tokenRefreshUrl',
|
|
240
|
+
from: redactUrl(serverA.tokenRefreshUrl),
|
|
241
|
+
to: redactUrl(serverB.tokenRefreshUrl),
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (stableStringify(serverA.tokenRefreshBody) !== stableStringify(serverB.tokenRefreshBody)) {
|
|
245
|
+
changes.push({
|
|
246
|
+
field: 'tokenRefreshBody',
|
|
247
|
+
from: redactDeep(serverA.tokenRefreshBody),
|
|
248
|
+
to: redactDeep(serverB.tokenRefreshBody),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
162
251
|
if (changes.length > 0) {
|
|
163
252
|
entries.push({ serverName: name, kind: 'modified', changes });
|
|
164
253
|
}
|
package/dist/redact.js
CHANGED
|
@@ -12,7 +12,7 @@ const REDACTED = '[REDACTED]';
|
|
|
12
12
|
/** Matches key names that conventionally carry secret values. Intentionally
|
|
13
13
|
* broad — false positives (redacting a harmless key) are safe; false
|
|
14
14
|
* negatives (leaking a secret) are not. */
|
|
15
|
-
const SECRET_KEY_PATTERN = /(authorization|auth|token|secret|password|passwd|pwd|api[-_]?key|apikey|cookie|credential|bearer|session|private[-_]?key|client[-_]?secret)/i;
|
|
15
|
+
const SECRET_KEY_PATTERN = /(authorization|auth|token|secret|password|passwd|pwd|api[-_]?key|apikey|access[-_]?key|access[-_]?token|(?:^|[-_])key(?:$|[-_])|cookie|credential|bearer|session|private[-_]?key|client[-_]?secret)/i;
|
|
16
16
|
export function isSecretKey(key) {
|
|
17
17
|
return SECRET_KEY_PATTERN.test(key);
|
|
18
18
|
}
|