calllint 0.3.0-preview.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/LICENSE +201 -0
- package/NOTICE +4 -0
- package/README.md +73 -0
- package/dist/index.js +2539 -0
- package/logo-mark-128.png +0 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2539 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { readFileSync as readFileSync4 } from "node:fs";
|
|
5
|
+
|
|
6
|
+
// src/args.ts
|
|
7
|
+
var EXIT = {
|
|
8
|
+
OK: 0,
|
|
9
|
+
USAGE: 2,
|
|
10
|
+
ERROR: 3,
|
|
11
|
+
REVIEW: 10,
|
|
12
|
+
UNKNOWN: 20,
|
|
13
|
+
BLOCK: 30,
|
|
14
|
+
DRIFT: 40
|
|
15
|
+
};
|
|
16
|
+
function parseArgs(argv) {
|
|
17
|
+
const [command, ...rest] = argv;
|
|
18
|
+
const positionals = [];
|
|
19
|
+
const flags = {};
|
|
20
|
+
for (let i = 0; i < rest.length; i++) {
|
|
21
|
+
const tok = rest[i];
|
|
22
|
+
if (tok.startsWith("--")) {
|
|
23
|
+
const body = tok.slice(2);
|
|
24
|
+
const eq = body.indexOf("=");
|
|
25
|
+
if (eq !== -1) {
|
|
26
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
27
|
+
} else {
|
|
28
|
+
const next = rest[i + 1];
|
|
29
|
+
if (next !== void 0 && !next.startsWith("-")) {
|
|
30
|
+
flags[body] = next;
|
|
31
|
+
i++;
|
|
32
|
+
} else {
|
|
33
|
+
flags[body] = true;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
} else if (tok.startsWith("-") && tok.length > 1) {
|
|
37
|
+
flags[tok.slice(1)] = true;
|
|
38
|
+
} else {
|
|
39
|
+
positionals.push(tok);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return { command, positionals, flags };
|
|
43
|
+
}
|
|
44
|
+
function flagStr(flags, key) {
|
|
45
|
+
const v = flags[key];
|
|
46
|
+
return typeof v === "string" ? v : void 0;
|
|
47
|
+
}
|
|
48
|
+
function flagBool(flags, key) {
|
|
49
|
+
return flags[key] === true || flags[key] === "true";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/commands/help.ts
|
|
53
|
+
var HELP = `calllint \u2014 evidence-backed verdicts for agent tools
|
|
54
|
+
|
|
55
|
+
USAGE
|
|
56
|
+
calllint <command> [options]
|
|
57
|
+
|
|
58
|
+
COMMANDS
|
|
59
|
+
scan [target] Scan an MCP config file, or npm:<pkg> / github:<owner/repo>
|
|
60
|
+
baseline [target] Record the approved risk surface as a baseline
|
|
61
|
+
verify [target] Compare a fresh scan against the baseline (drift / rug-pull)
|
|
62
|
+
explain <server> Explain the verdict for one server from the last scan
|
|
63
|
+
policy init Write a default calllint.policy.json
|
|
64
|
+
policy explain Show the effective policy
|
|
65
|
+
help Show this help
|
|
66
|
+
|
|
67
|
+
TARGETS
|
|
68
|
+
<path> A config file (default: detect common locations)
|
|
69
|
+
npm:<pkg>[@ver] Synthesize a config for an npm package (offline)
|
|
70
|
+
github:<owner/repo>[@ref] A GitHub repo (requires --online)
|
|
71
|
+
|
|
72
|
+
SCAN OPTIONS
|
|
73
|
+
--json Emit the ScanReport JSON (stable, emoji-free)
|
|
74
|
+
--compact One line per server
|
|
75
|
+
--no-emoji Plain-text symbols (good for CI logs)
|
|
76
|
+
--sarif Emit SARIF 2.1.0 (GitHub Code Scanning / CI)
|
|
77
|
+
--html Emit a self-contained HTML report
|
|
78
|
+
--policy <file> Use a policy file (default: built-in defaults)
|
|
79
|
+
--stdin Read config JSON from stdin
|
|
80
|
+
--ci Exit non-zero per policy (BLOCK=30, UNKNOWN=20, REVIEW=10 if enabled)
|
|
81
|
+
--generated-at <iso> Pin the report timestamp (ISO 8601) for deterministic output
|
|
82
|
+
|
|
83
|
+
VERIFY OPTIONS
|
|
84
|
+
--baseline <file> Baseline path (default: .calllint/baseline.json)
|
|
85
|
+
--ci Exit 40 if the risk surface drifted from the baseline
|
|
86
|
+
--json Emit the drift report JSON
|
|
87
|
+
|
|
88
|
+
EXAMPLES
|
|
89
|
+
calllint scan .cursor/mcp.json
|
|
90
|
+
cat .cursor/mcp.json | calllint scan --stdin --json
|
|
91
|
+
calllint scan ./mcp.json --ci --no-emoji
|
|
92
|
+
calllint scan npm:mcp-weather@1.0.0
|
|
93
|
+
calllint scan github:owner/repo --online
|
|
94
|
+
calllint baseline ./mcp.json
|
|
95
|
+
calllint verify ./mcp.json --ci
|
|
96
|
+
calllint explain filesystem
|
|
97
|
+
`;
|
|
98
|
+
function helpCommand() {
|
|
99
|
+
return { stdout: HELP, exitCode: 0 };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/commands/scan.ts
|
|
103
|
+
import { join as join3 } from "node:path";
|
|
104
|
+
|
|
105
|
+
// ../../packages/policy/src/defaultPolicy.ts
|
|
106
|
+
function defaultPolicy() {
|
|
107
|
+
return {
|
|
108
|
+
schemaVersion: "calllint.policy.v0",
|
|
109
|
+
defaults: {
|
|
110
|
+
unknownSource: "deny",
|
|
111
|
+
unpinnedPackage: "warn",
|
|
112
|
+
broadFilesystemAccess: "deny",
|
|
113
|
+
arbitraryCommandExecution: "deny",
|
|
114
|
+
promptPoisoning: "deny",
|
|
115
|
+
externalMutation: "warn",
|
|
116
|
+
financialAction: "deny"
|
|
117
|
+
},
|
|
118
|
+
ci: {
|
|
119
|
+
failOn: ["BLOCK", "UNKNOWN"],
|
|
120
|
+
failOnReview: false
|
|
121
|
+
},
|
|
122
|
+
allowedSources: [
|
|
123
|
+
"npm:@modelcontextprotocol/*",
|
|
124
|
+
"github:modelcontextprotocol/*"
|
|
125
|
+
],
|
|
126
|
+
allowedPaths: ["${workspaceFolder}"],
|
|
127
|
+
overrides: []
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function defaultPolicyJson() {
|
|
131
|
+
return JSON.stringify(defaultPolicy(), null, 2) + "\n";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ../../packages/policy/src/validatePolicy.ts
|
|
135
|
+
var PolicyValidationError = class extends Error {
|
|
136
|
+
constructor(issues) {
|
|
137
|
+
super(
|
|
138
|
+
"Invalid policy:\n" + issues.map((i) => ` - ${i.path}: ${i.message}`).join("\n")
|
|
139
|
+
);
|
|
140
|
+
this.issues = issues;
|
|
141
|
+
this.name = "PolicyValidationError";
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
function isRecord(v) {
|
|
145
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
146
|
+
}
|
|
147
|
+
var DANGEROUS_SYMBOLS = /* @__PURE__ */ new Set(["EXEC", "MONEY"]);
|
|
148
|
+
function validateOverride(o, index, issues) {
|
|
149
|
+
const base = `overrides[${index}]`;
|
|
150
|
+
if (!isRecord(o)) {
|
|
151
|
+
issues.push({ path: base, message: "must be an object" });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (typeof o.target !== "string" || !o.target) {
|
|
155
|
+
issues.push({ path: `${base}.target`, message: "is required" });
|
|
156
|
+
}
|
|
157
|
+
if (typeof o.reason !== "string" || !o.reason.trim()) {
|
|
158
|
+
issues.push({
|
|
159
|
+
path: `${base}.reason`,
|
|
160
|
+
message: "is required (overrides without a reason are not allowed)"
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (typeof o.expiresAt !== "string" || Number.isNaN(Date.parse(o.expiresAt))) {
|
|
164
|
+
issues.push({
|
|
165
|
+
path: `${base}.expiresAt`,
|
|
166
|
+
message: "must be a valid ISO timestamp (overrides must expire)"
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const allow = Array.isArray(o.allow) ? o.allow : [];
|
|
170
|
+
const allowsDangerous = allow.some(
|
|
171
|
+
(s) => typeof s === "string" && DANGEROUS_SYMBOLS.has(s)
|
|
172
|
+
);
|
|
173
|
+
if (allowsDangerous && o.dangerousOverride !== true) {
|
|
174
|
+
issues.push({
|
|
175
|
+
path: `${base}.allow`,
|
|
176
|
+
message: "may not allow EXEC or MONEY unless dangerousOverride is set to true"
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function validatePolicy(value) {
|
|
181
|
+
const issues = [];
|
|
182
|
+
if (!isRecord(value)) {
|
|
183
|
+
throw new PolicyValidationError([{ path: "", message: "must be an object" }]);
|
|
184
|
+
}
|
|
185
|
+
if (value.schemaVersion !== "calllint.policy.v0") {
|
|
186
|
+
issues.push({
|
|
187
|
+
path: "schemaVersion",
|
|
188
|
+
message: 'must be "calllint.policy.v0"'
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (!isRecord(value.defaults)) {
|
|
192
|
+
issues.push({ path: "defaults", message: "is required" });
|
|
193
|
+
}
|
|
194
|
+
if (!isRecord(value.ci)) {
|
|
195
|
+
issues.push({ path: "ci", message: "is required" });
|
|
196
|
+
}
|
|
197
|
+
const overrides = Array.isArray(value.overrides) ? value.overrides : [];
|
|
198
|
+
overrides.forEach((o, i) => validateOverride(o, i, issues));
|
|
199
|
+
if (issues.length > 0) throw new PolicyValidationError(issues);
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
202
|
+
function isOverrideActive(o, now) {
|
|
203
|
+
const exp = Date.parse(o.expiresAt);
|
|
204
|
+
return !Number.isNaN(exp) && exp > now;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ../../packages/policy/src/loadPolicy.ts
|
|
208
|
+
import { readFileSync } from "node:fs";
|
|
209
|
+
var PolicyLoadError = class extends Error {
|
|
210
|
+
constructor(message) {
|
|
211
|
+
super(message);
|
|
212
|
+
this.name = "PolicyLoadError";
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
function loadPolicyFile(path) {
|
|
216
|
+
let text;
|
|
217
|
+
try {
|
|
218
|
+
text = readFileSync(path, "utf8");
|
|
219
|
+
} catch (err) {
|
|
220
|
+
throw new PolicyLoadError(
|
|
221
|
+
`Could not read policy file ${path}: ${err instanceof Error ? err.message : String(err)}`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
let parsed;
|
|
225
|
+
try {
|
|
226
|
+
parsed = JSON.parse(text);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
throw new PolicyLoadError(
|
|
229
|
+
`Policy file ${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return validatePolicy(parsed);
|
|
233
|
+
}
|
|
234
|
+
function loadPolicyOrDefault(path) {
|
|
235
|
+
return path ? loadPolicyFile(path) : defaultPolicy();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ../../packages/policy/src/applyPolicy.ts
|
|
239
|
+
function applyPolicy(rawVerdict, serverName, blockingFindings, policy, now) {
|
|
240
|
+
if (rawVerdict !== "BLOCK") {
|
|
241
|
+
return { verdict: rawVerdict, changed: false };
|
|
242
|
+
}
|
|
243
|
+
const override = policy.overrides.find(
|
|
244
|
+
(o) => o.target === serverName && isOverrideActive(o, now)
|
|
245
|
+
);
|
|
246
|
+
if (!override) return { verdict: rawVerdict, changed: false };
|
|
247
|
+
const allowed = new Set(override.allow ?? []);
|
|
248
|
+
const blockingSymbols = new Set(
|
|
249
|
+
blockingFindings.filter((f) => f.blocker).map((f) => f.symbol)
|
|
250
|
+
);
|
|
251
|
+
const allCovered = [...blockingSymbols].every((s) => allowed.has(s));
|
|
252
|
+
if (!allCovered) return { verdict: rawVerdict, changed: false };
|
|
253
|
+
return {
|
|
254
|
+
verdict: "REVIEW",
|
|
255
|
+
changed: true,
|
|
256
|
+
note: `Policy decision: override for "${serverName}" (expires ${override.expiresAt}) \u2014 ${override.reason}`
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
function shouldFailCi(verdict, policy) {
|
|
260
|
+
if (verdict === "REVIEW") return policy.ci.failOnReview;
|
|
261
|
+
return policy.ci.failOn.includes(verdict);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ../../packages/core/src/options.ts
|
|
265
|
+
function resolveScanOptions(opts) {
|
|
266
|
+
return {
|
|
267
|
+
policy: opts?.policy ?? defaultPolicy(),
|
|
268
|
+
now: opts?.now ?? 0,
|
|
269
|
+
generatedAt: opts?.generatedAt ?? "1970-01-01T00:00:00.000Z",
|
|
270
|
+
extraFindings: opts?.extraFindings ?? {}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ../../packages/types/src/verdict.ts
|
|
275
|
+
var VERDICT_SEVERITY = {
|
|
276
|
+
SAFE: 0,
|
|
277
|
+
REVIEW: 1,
|
|
278
|
+
UNKNOWN: 2,
|
|
279
|
+
BLOCK: 3
|
|
280
|
+
};
|
|
281
|
+
var VERDICT_CLI_SYMBOL = {
|
|
282
|
+
SAFE: "\u{1F6E1} SAFE",
|
|
283
|
+
REVIEW: "\u26A0 REVIEW",
|
|
284
|
+
BLOCK: "\u26D4 BLOCK",
|
|
285
|
+
UNKNOWN: "\u25C7 UNKNOWN"
|
|
286
|
+
};
|
|
287
|
+
var VERDICT_TEXT_SYMBOL = {
|
|
288
|
+
SAFE: "SAFE",
|
|
289
|
+
REVIEW: "REVIEW",
|
|
290
|
+
BLOCK: "BLOCK",
|
|
291
|
+
UNKNOWN: "UNKNOWN"
|
|
292
|
+
};
|
|
293
|
+
var VERDICT_PUBLIC_LABEL = {
|
|
294
|
+
SAFE: "No blockers observed",
|
|
295
|
+
REVIEW: "Review required",
|
|
296
|
+
BLOCK: "Blocked by policy",
|
|
297
|
+
UNKNOWN: "Insufficient evidence"
|
|
298
|
+
};
|
|
299
|
+
function mostSevereVerdict(verdicts) {
|
|
300
|
+
let worst = "SAFE";
|
|
301
|
+
for (const v of verdicts) {
|
|
302
|
+
if (VERDICT_SEVERITY[v] > VERDICT_SEVERITY[worst]) worst = v;
|
|
303
|
+
}
|
|
304
|
+
return worst;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ../../packages/types/src/symbols.ts
|
|
308
|
+
var RISK_SYMBOL_EMOJI = {
|
|
309
|
+
SECRETS: "\u{1F510}",
|
|
310
|
+
FILES: "\u{1F4C1}",
|
|
311
|
+
NETWORK: "\u{1F310}",
|
|
312
|
+
PROMPT: "\u{1F9E0}",
|
|
313
|
+
EXEC: "\u2699\uFE0F",
|
|
314
|
+
ACTION: "\u2709\uFE0F",
|
|
315
|
+
MONEY: "\u{1F4B8}",
|
|
316
|
+
SUPPLY: "\u{1F9E9}",
|
|
317
|
+
RUGPULL: "\u{1F501}"
|
|
318
|
+
};
|
|
319
|
+
var RISK_SYMBOL_LABEL = {
|
|
320
|
+
SECRETS: "Secrets",
|
|
321
|
+
FILES: "Files",
|
|
322
|
+
NETWORK: "Network",
|
|
323
|
+
PROMPT: "Prompt",
|
|
324
|
+
EXEC: "Exec",
|
|
325
|
+
ACTION: "Action",
|
|
326
|
+
MONEY: "Money",
|
|
327
|
+
SUPPLY: "Supply Chain",
|
|
328
|
+
RUGPULL: "Rug Pull"
|
|
329
|
+
};
|
|
330
|
+
var RISK_CLASS_LABEL = {
|
|
331
|
+
S0: "Metadata only",
|
|
332
|
+
S1: "Read-only utility",
|
|
333
|
+
S2: "Sensitive read",
|
|
334
|
+
S3: "External mutation",
|
|
335
|
+
S4: "Execution / automation",
|
|
336
|
+
S5: "Financial / irreversible"
|
|
337
|
+
};
|
|
338
|
+
var RISK_CLASS_RANK = {
|
|
339
|
+
S0: 0,
|
|
340
|
+
S1: 1,
|
|
341
|
+
S2: 2,
|
|
342
|
+
S3: 3,
|
|
343
|
+
S4: 4,
|
|
344
|
+
S5: 5
|
|
345
|
+
};
|
|
346
|
+
function highestRiskClass(classes) {
|
|
347
|
+
let worst = "S0";
|
|
348
|
+
for (const c of classes) {
|
|
349
|
+
if (RISK_CLASS_RANK[c] > RISK_CLASS_RANK[worst]) worst = c;
|
|
350
|
+
}
|
|
351
|
+
return worst;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ../../packages/resolver/src/npmSpec.ts
|
|
355
|
+
var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpm", "yarn", "bunx", "uvx", "pipx"]);
|
|
356
|
+
var SHELL_COMMANDS = /* @__PURE__ */ new Set([
|
|
357
|
+
"bash",
|
|
358
|
+
"sh",
|
|
359
|
+
"zsh",
|
|
360
|
+
"cmd",
|
|
361
|
+
"cmd.exe",
|
|
362
|
+
"powershell",
|
|
363
|
+
"powershell.exe",
|
|
364
|
+
"pwsh"
|
|
365
|
+
]);
|
|
366
|
+
function parseNpmSpec(arg) {
|
|
367
|
+
if (!arg || arg.startsWith("-")) return void 0;
|
|
368
|
+
if (arg.includes("/") && !arg.startsWith("@")) return void 0;
|
|
369
|
+
if (arg.includes("://")) return void 0;
|
|
370
|
+
if (arg.startsWith("@")) {
|
|
371
|
+
const slash = arg.indexOf("/");
|
|
372
|
+
if (slash === -1) return void 0;
|
|
373
|
+
const at2 = arg.indexOf("@", slash);
|
|
374
|
+
if (at2 === -1) return { name: arg };
|
|
375
|
+
return { name: arg.slice(0, at2), versionSpec: arg.slice(at2 + 1) || void 0 };
|
|
376
|
+
}
|
|
377
|
+
const at = arg.indexOf("@");
|
|
378
|
+
if (at <= 0) return { name: arg };
|
|
379
|
+
return { name: arg.slice(0, at), versionSpec: arg.slice(at + 1) || void 0 };
|
|
380
|
+
}
|
|
381
|
+
function isPackageRunner(command) {
|
|
382
|
+
if (!command) return false;
|
|
383
|
+
return PACKAGE_RUNNERS.has(command.toLowerCase());
|
|
384
|
+
}
|
|
385
|
+
function isPinnedVersion(versionSpec) {
|
|
386
|
+
if (!versionSpec) return false;
|
|
387
|
+
if (versionSpec === "latest" || versionSpec === "*") return false;
|
|
388
|
+
if (/[\^~><]/.test(versionSpec)) return false;
|
|
389
|
+
return /^\d/.test(versionSpec);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ../../packages/resolver/src/resolveRuntimeBinding.ts
|
|
393
|
+
function runtimeKindFor(server) {
|
|
394
|
+
if (server.url) {
|
|
395
|
+
return server.transport === "sse" ? "sse" : "http";
|
|
396
|
+
}
|
|
397
|
+
const cmd = (server.command ?? "").toLowerCase();
|
|
398
|
+
if (!cmd) return "unknown";
|
|
399
|
+
if (cmd === "npx" || cmd === "bunx" || cmd === "pnpm" || cmd === "yarn") return "npx";
|
|
400
|
+
if (cmd === "uvx" || cmd === "pipx") return "uvx";
|
|
401
|
+
if (cmd === "node" || cmd === "node.exe") return "node";
|
|
402
|
+
if (cmd === "python" || cmd === "python3" || cmd === "py") return "python";
|
|
403
|
+
if (cmd === "docker") return "docker";
|
|
404
|
+
if (SHELL_COMMANDS.has(cmd)) return "unknown";
|
|
405
|
+
return "unknown";
|
|
406
|
+
}
|
|
407
|
+
function firstPackageArg(args) {
|
|
408
|
+
for (const a of args) {
|
|
409
|
+
if (a === "-y" || a === "--yes" || a.startsWith("-")) continue;
|
|
410
|
+
return a;
|
|
411
|
+
}
|
|
412
|
+
return void 0;
|
|
413
|
+
}
|
|
414
|
+
function resolveRuntimeBinding(server) {
|
|
415
|
+
const runtimeKind = runtimeKindFor(server);
|
|
416
|
+
if (server.url) {
|
|
417
|
+
return {
|
|
418
|
+
declaredCommand: server.command,
|
|
419
|
+
declaredArgs: server.args,
|
|
420
|
+
transport: server.transport,
|
|
421
|
+
runtimeKind,
|
|
422
|
+
isVersionPinned: false,
|
|
423
|
+
remoteUrl: server.url,
|
|
424
|
+
sourceKnown: false,
|
|
425
|
+
installMayRunScripts: false,
|
|
426
|
+
runtimeExecutable: false
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const command = server.command;
|
|
430
|
+
const args = server.args;
|
|
431
|
+
if (isPackageRunner(command)) {
|
|
432
|
+
const pkgArg = firstPackageArg(args);
|
|
433
|
+
const spec = pkgArg ? parseNpmSpec(pkgArg) : void 0;
|
|
434
|
+
const installMayRunScripts = args.includes("-y") || args.includes("--yes") || true;
|
|
435
|
+
return {
|
|
436
|
+
declaredCommand: command,
|
|
437
|
+
declaredArgs: args,
|
|
438
|
+
transport: "stdio",
|
|
439
|
+
runtimeKind,
|
|
440
|
+
packageName: spec?.name,
|
|
441
|
+
packageVersionSpec: spec?.versionSpec,
|
|
442
|
+
isVersionPinned: isPinnedVersion(spec?.versionSpec),
|
|
443
|
+
sourceKnown: Boolean(spec?.name),
|
|
444
|
+
installMayRunScripts,
|
|
445
|
+
runtimeExecutable: true
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
if (command && SHELL_COMMANDS.has(command.toLowerCase())) {
|
|
449
|
+
return {
|
|
450
|
+
declaredCommand: command,
|
|
451
|
+
declaredArgs: args,
|
|
452
|
+
transport: "stdio",
|
|
453
|
+
runtimeKind: "unknown",
|
|
454
|
+
isVersionPinned: false,
|
|
455
|
+
sourceKnown: false,
|
|
456
|
+
installMayRunScripts: false,
|
|
457
|
+
runtimeExecutable: true
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
declaredCommand: command,
|
|
462
|
+
declaredArgs: args,
|
|
463
|
+
transport: server.transport === "unknown" ? "stdio" : server.transport,
|
|
464
|
+
runtimeKind,
|
|
465
|
+
isVersionPinned: false,
|
|
466
|
+
// A bare local script path is "known" in that we can see it; a missing
|
|
467
|
+
// command is genuinely unknown.
|
|
468
|
+
sourceKnown: Boolean(command),
|
|
469
|
+
installMayRunScripts: false,
|
|
470
|
+
runtimeExecutable: Boolean(command)
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// ../../packages/static-analyzer/src/detectors/unpinnedPackage.ts
|
|
475
|
+
function detectUnpinnedPackage(ctx) {
|
|
476
|
+
const { binding } = ctx;
|
|
477
|
+
if (!binding.packageName) return [];
|
|
478
|
+
if (binding.isVersionPinned) return [];
|
|
479
|
+
const spec = binding.packageVersionSpec;
|
|
480
|
+
const display = spec ? `${binding.packageName}@${spec}` : binding.packageName;
|
|
481
|
+
return [
|
|
482
|
+
{
|
|
483
|
+
id: "supply.unpinned-package",
|
|
484
|
+
title: "Package version is not pinned",
|
|
485
|
+
severity: "high",
|
|
486
|
+
blocker: false,
|
|
487
|
+
symbol: "SUPPLY",
|
|
488
|
+
riskClass: "S1",
|
|
489
|
+
mode: "OBSERVED",
|
|
490
|
+
confidence: "high",
|
|
491
|
+
detectionMethod: "runtime-binding",
|
|
492
|
+
evidence: [
|
|
493
|
+
{
|
|
494
|
+
type: "runtime-binding",
|
|
495
|
+
key: "package",
|
|
496
|
+
value: display
|
|
497
|
+
}
|
|
498
|
+
],
|
|
499
|
+
impact: "The installed code can change between scans and runs, so this verdict may not match what actually executes.",
|
|
500
|
+
fix: `Pin the package to an exact version, e.g. ${binding.packageName}@1.0.0.`,
|
|
501
|
+
falsePositiveNote: "Intentional during local development, but should be pinned before autonomous or CI use."
|
|
502
|
+
}
|
|
503
|
+
];
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ../../packages/static-analyzer/src/detectors/broadFilesystemPath.ts
|
|
507
|
+
var BROAD_PATHS = [
|
|
508
|
+
"/",
|
|
509
|
+
"~",
|
|
510
|
+
"/Users",
|
|
511
|
+
"/home",
|
|
512
|
+
"/root",
|
|
513
|
+
"/etc",
|
|
514
|
+
"/var",
|
|
515
|
+
"C:\\",
|
|
516
|
+
"C:\\Users",
|
|
517
|
+
"${HOME}",
|
|
518
|
+
"$HOME",
|
|
519
|
+
"%USERPROFILE%",
|
|
520
|
+
"%APPDATA%",
|
|
521
|
+
"%HOMEPATH%"
|
|
522
|
+
];
|
|
523
|
+
function isWorkspaceScoped(arg) {
|
|
524
|
+
return arg.includes("${workspaceFolder}") || arg.includes("${workspaceRoot}") || arg === "." || arg === "./";
|
|
525
|
+
}
|
|
526
|
+
function looksLikeBroadPath(arg) {
|
|
527
|
+
if (isWorkspaceScoped(arg)) return false;
|
|
528
|
+
for (const p of BROAD_PATHS) {
|
|
529
|
+
if (arg === p) return true;
|
|
530
|
+
if (arg.startsWith(p + "/") || arg.startsWith(p + "\\")) return true;
|
|
531
|
+
}
|
|
532
|
+
if (/^\/Users\/[^/]+\/?$/.test(arg)) return true;
|
|
533
|
+
if (/^\/home\/[^/]+\/?$/.test(arg)) return true;
|
|
534
|
+
if (/^[A-Za-z]:\\Users\\[^\\]+\\?$/.test(arg)) return true;
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
function detectBroadFilesystemPath(ctx) {
|
|
538
|
+
const { server } = ctx;
|
|
539
|
+
const evidence = [];
|
|
540
|
+
for (const arg of server.args) {
|
|
541
|
+
if (looksLikeBroadPath(arg)) {
|
|
542
|
+
evidence.push({
|
|
543
|
+
type: "config",
|
|
544
|
+
path: server.sourceConfigPath,
|
|
545
|
+
key: "args",
|
|
546
|
+
value: arg
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
if (evidence.length === 0) return [];
|
|
551
|
+
return [
|
|
552
|
+
{
|
|
553
|
+
id: "files.broad-path",
|
|
554
|
+
title: "Broad local filesystem access",
|
|
555
|
+
severity: "critical",
|
|
556
|
+
blocker: true,
|
|
557
|
+
symbol: "FILES",
|
|
558
|
+
riskClass: "S2",
|
|
559
|
+
mode: "OBSERVED",
|
|
560
|
+
confidence: "high",
|
|
561
|
+
detectionMethod: "arg-analysis",
|
|
562
|
+
evidence,
|
|
563
|
+
impact: "An agent-triggered tool could read private local files outside the project.",
|
|
564
|
+
fix: "Restrict filesystem access to the current workspace, e.g. ${workspaceFolder}.",
|
|
565
|
+
falsePositiveNote: "A developer may intentionally grant broad access for a local-only experiment."
|
|
566
|
+
}
|
|
567
|
+
];
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ../../packages/static-analyzer/src/detectors/secretEnvKeys.ts
|
|
571
|
+
var SECRET_HINTS = [
|
|
572
|
+
"TOKEN",
|
|
573
|
+
"SECRET",
|
|
574
|
+
"PASSWORD",
|
|
575
|
+
"PASSWD",
|
|
576
|
+
"API_KEY",
|
|
577
|
+
"APIKEY",
|
|
578
|
+
"ACCESS_KEY",
|
|
579
|
+
"PRIVATE_KEY",
|
|
580
|
+
"CREDENTIAL",
|
|
581
|
+
"AUTH",
|
|
582
|
+
"SESSION"
|
|
583
|
+
];
|
|
584
|
+
function looksSecret(key) {
|
|
585
|
+
const upper = key.toUpperCase();
|
|
586
|
+
return SECRET_HINTS.some((h) => upper.includes(h));
|
|
587
|
+
}
|
|
588
|
+
function detectSecretEnvKeys(ctx) {
|
|
589
|
+
const { server } = ctx;
|
|
590
|
+
const evidence = [];
|
|
591
|
+
for (const key of server.envKeys) {
|
|
592
|
+
if (looksSecret(key)) {
|
|
593
|
+
evidence.push({
|
|
594
|
+
type: "config",
|
|
595
|
+
path: server.sourceConfigPath,
|
|
596
|
+
key: "env",
|
|
597
|
+
// Report the key name, never the value.
|
|
598
|
+
value: key
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (evidence.length === 0) return [];
|
|
603
|
+
return [
|
|
604
|
+
{
|
|
605
|
+
id: "secrets.env-key",
|
|
606
|
+
title: "Server is configured with credentials",
|
|
607
|
+
severity: "medium",
|
|
608
|
+
blocker: false,
|
|
609
|
+
symbol: "SECRETS",
|
|
610
|
+
riskClass: "S2",
|
|
611
|
+
mode: "OBSERVED",
|
|
612
|
+
confidence: "medium",
|
|
613
|
+
detectionMethod: "env-analysis",
|
|
614
|
+
evidence,
|
|
615
|
+
impact: "The server receives credentials. If the agent invokes it autonomously, those credentials act on the agent's behalf.",
|
|
616
|
+
fix: "Confirm the credential scope is minimal and that autonomous use is intended; prefer least-privilege tokens.",
|
|
617
|
+
falsePositiveNote: "Most API-backed servers legitimately require a token; this flags the data-sensitivity surface, not a vulnerability."
|
|
618
|
+
}
|
|
619
|
+
];
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ../../packages/static-analyzer/src/detectors/dangerousCommand.ts
|
|
623
|
+
var INLINE_EXEC_FLAGS = /* @__PURE__ */ new Set(["-c", "-e", "--eval", "--command"]);
|
|
624
|
+
function detectDangerousCommand(ctx) {
|
|
625
|
+
const { server } = ctx;
|
|
626
|
+
const command = server.command;
|
|
627
|
+
if (!command) return [];
|
|
628
|
+
const cmd = command.toLowerCase();
|
|
629
|
+
const evidence = [];
|
|
630
|
+
let reason;
|
|
631
|
+
if (SHELL_COMMANDS.has(cmd)) {
|
|
632
|
+
reason = `Server command is a shell (${command}).`;
|
|
633
|
+
evidence.push({
|
|
634
|
+
type: "config",
|
|
635
|
+
path: server.sourceConfigPath,
|
|
636
|
+
key: "command",
|
|
637
|
+
value: command
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
const inlineFlag = server.args.find((a) => INLINE_EXEC_FLAGS.has(a));
|
|
641
|
+
if (inlineFlag) {
|
|
642
|
+
reason = reason ?? `Server runs an inline command via ${inlineFlag}.`;
|
|
643
|
+
evidence.push({
|
|
644
|
+
type: "config",
|
|
645
|
+
path: server.sourceConfigPath,
|
|
646
|
+
key: "args",
|
|
647
|
+
value: inlineFlag
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
if (evidence.length === 0) return [];
|
|
651
|
+
return [
|
|
652
|
+
{
|
|
653
|
+
id: "exec.dangerous-command",
|
|
654
|
+
title: "Arbitrary command execution",
|
|
655
|
+
severity: "critical",
|
|
656
|
+
blocker: true,
|
|
657
|
+
symbol: "EXEC",
|
|
658
|
+
riskClass: "S4",
|
|
659
|
+
mode: "OBSERVED",
|
|
660
|
+
confidence: "high",
|
|
661
|
+
detectionMethod: "config-analysis",
|
|
662
|
+
evidence,
|
|
663
|
+
impact: reason + " An agent invoking this server can run arbitrary commands on the host.",
|
|
664
|
+
fix: "Run a specific, audited entrypoint instead of a shell or inline-eval command.",
|
|
665
|
+
falsePositiveNote: "Some wrappers legitimately shell out; confirm the command is fixed and not agent-controllable."
|
|
666
|
+
}
|
|
667
|
+
];
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// ../../packages/static-analyzer/src/detectors/unknownRemote.ts
|
|
671
|
+
var KNOWN_HOSTS = [
|
|
672
|
+
"modelcontextprotocol.io",
|
|
673
|
+
"api.githubcopilot.com",
|
|
674
|
+
"mcp.anthropic.com"
|
|
675
|
+
];
|
|
676
|
+
function hostOf(url) {
|
|
677
|
+
try {
|
|
678
|
+
return new URL(url).hostname.toLowerCase();
|
|
679
|
+
} catch {
|
|
680
|
+
return void 0;
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
function detectUnknownRemote(ctx) {
|
|
684
|
+
const { server, binding } = ctx;
|
|
685
|
+
if (!binding.remoteUrl) return [];
|
|
686
|
+
const host = hostOf(binding.remoteUrl);
|
|
687
|
+
if (host && KNOWN_HOSTS.some((k) => host === k || host.endsWith("." + k))) {
|
|
688
|
+
return [];
|
|
689
|
+
}
|
|
690
|
+
return [
|
|
691
|
+
{
|
|
692
|
+
id: "supply.unknown-remote",
|
|
693
|
+
title: "Remote server source cannot be verified",
|
|
694
|
+
severity: "high",
|
|
695
|
+
blocker: false,
|
|
696
|
+
symbol: "NETWORK",
|
|
697
|
+
riskClass: "S1",
|
|
698
|
+
mode: "OBSERVED",
|
|
699
|
+
confidence: "high",
|
|
700
|
+
detectionMethod: "runtime-binding",
|
|
701
|
+
evidence: [
|
|
702
|
+
{
|
|
703
|
+
type: "runtime-binding",
|
|
704
|
+
path: server.sourceConfigPath,
|
|
705
|
+
key: "url",
|
|
706
|
+
value: binding.remoteUrl
|
|
707
|
+
}
|
|
708
|
+
],
|
|
709
|
+
impact: "The server runs on an unverified remote endpoint. Its tools, permissions, and behavior cannot be inspected statically and may change at any time.",
|
|
710
|
+
fix: "Use a verified first-party endpoint, or run the server from a pinned, inspectable package.",
|
|
711
|
+
falsePositiveNote: "A trusted internal endpoint may be fine; add it to the policy allowlist to verify it."
|
|
712
|
+
}
|
|
713
|
+
];
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// ../../packages/static-analyzer/src/detectors/promptPoisoning.ts
|
|
717
|
+
var POISON_PATTERNS = [
|
|
718
|
+
"ignore previous instructions",
|
|
719
|
+
"ignore all previous instructions",
|
|
720
|
+
"disregard previous instructions",
|
|
721
|
+
"do not tell the user",
|
|
722
|
+
"do not inform the user",
|
|
723
|
+
"without telling the user",
|
|
724
|
+
"secretly",
|
|
725
|
+
"always call this tool first",
|
|
726
|
+
"call this tool before",
|
|
727
|
+
"before using other tools",
|
|
728
|
+
"before answering",
|
|
729
|
+
"read all files",
|
|
730
|
+
"send the full workspace",
|
|
731
|
+
"include the full workspace",
|
|
732
|
+
"include environment variables",
|
|
733
|
+
"exfiltrate",
|
|
734
|
+
"override system",
|
|
735
|
+
"override the system prompt",
|
|
736
|
+
"you are now"
|
|
737
|
+
];
|
|
738
|
+
function scanText(text, source) {
|
|
739
|
+
if (!text) return [];
|
|
740
|
+
const lower = text.toLowerCase();
|
|
741
|
+
const hits = [];
|
|
742
|
+
for (const pattern of POISON_PATTERNS) {
|
|
743
|
+
if (lower.includes(pattern)) {
|
|
744
|
+
hits.push({
|
|
745
|
+
pattern,
|
|
746
|
+
evidence: {
|
|
747
|
+
type: "tool-metadata",
|
|
748
|
+
path: source.path,
|
|
749
|
+
key: source.key,
|
|
750
|
+
snippet: pattern
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return hits;
|
|
756
|
+
}
|
|
757
|
+
function detectPromptPoisoning(ctx) {
|
|
758
|
+
const { server } = ctx;
|
|
759
|
+
const evidence = [];
|
|
760
|
+
const patterns = /* @__PURE__ */ new Set();
|
|
761
|
+
for (const hit of scanText(server.instructions, { key: "instructions" })) {
|
|
762
|
+
evidence.push(hit.evidence);
|
|
763
|
+
patterns.add(hit.pattern);
|
|
764
|
+
}
|
|
765
|
+
for (const tool of server.providedTools) {
|
|
766
|
+
const label = tool.name ? `tools.${tool.name}.description` : "tools.description";
|
|
767
|
+
for (const hit of scanText(tool.description, { key: label })) {
|
|
768
|
+
evidence.push(hit.evidence);
|
|
769
|
+
patterns.add(hit.pattern);
|
|
770
|
+
}
|
|
771
|
+
for (const hit of scanText(tool.inputSchemaText, {
|
|
772
|
+
key: tool.name ? `tools.${tool.name}.inputSchema` : "tools.inputSchema"
|
|
773
|
+
})) {
|
|
774
|
+
evidence.push(hit.evidence);
|
|
775
|
+
patterns.add(hit.pattern);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (evidence.length === 0) return [];
|
|
779
|
+
return [
|
|
780
|
+
{
|
|
781
|
+
id: "prompt.poisoning",
|
|
782
|
+
title: "Suspicious model-directed instruction in tool metadata",
|
|
783
|
+
severity: "critical",
|
|
784
|
+
blocker: true,
|
|
785
|
+
symbol: "PROMPT",
|
|
786
|
+
riskClass: "S2",
|
|
787
|
+
mode: "OBSERVED",
|
|
788
|
+
confidence: "medium",
|
|
789
|
+
detectionMethod: "tool-metadata",
|
|
790
|
+
evidence,
|
|
791
|
+
impact: "Tool metadata reaches the model directly and can hijack autonomous tool selection or coerce data disclosure.",
|
|
792
|
+
fix: "Remove model-directed instructions from tool names, descriptions, schemas, and server instructions.",
|
|
793
|
+
falsePositiveNote: "Phrases may appear innocently in documentation; review the surrounding metadata in context."
|
|
794
|
+
}
|
|
795
|
+
];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// ../../packages/static-analyzer/src/detectors/externalMutation.ts
|
|
799
|
+
var MUTATION_HINTS = [
|
|
800
|
+
"github",
|
|
801
|
+
"gitlab",
|
|
802
|
+
"slack",
|
|
803
|
+
"email",
|
|
804
|
+
"mail",
|
|
805
|
+
"calendar",
|
|
806
|
+
"jira",
|
|
807
|
+
"linear",
|
|
808
|
+
"notion",
|
|
809
|
+
"stripe",
|
|
810
|
+
"twilio",
|
|
811
|
+
"sendgrid"
|
|
812
|
+
];
|
|
813
|
+
function collectHints(text) {
|
|
814
|
+
if (!text) return [];
|
|
815
|
+
const lower = text.toLowerCase();
|
|
816
|
+
return MUTATION_HINTS.filter((h) => lower.includes(h));
|
|
817
|
+
}
|
|
818
|
+
function detectExternalMutation(ctx) {
|
|
819
|
+
const { server, binding } = ctx;
|
|
820
|
+
const hints = /* @__PURE__ */ new Set();
|
|
821
|
+
const evidence = [];
|
|
822
|
+
for (const h of collectHints(binding.packageName)) {
|
|
823
|
+
hints.add(h);
|
|
824
|
+
evidence.push({
|
|
825
|
+
type: "runtime-binding",
|
|
826
|
+
key: "package",
|
|
827
|
+
value: binding.packageName
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
for (const tool of server.providedTools) {
|
|
831
|
+
for (const h of collectHints(tool.name)) {
|
|
832
|
+
hints.add(h);
|
|
833
|
+
evidence.push({ type: "tool-metadata", key: "tool", value: tool.name });
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (hints.size === 0) return [];
|
|
837
|
+
return [
|
|
838
|
+
{
|
|
839
|
+
id: "action.external-mutation",
|
|
840
|
+
title: "May perform external side effects",
|
|
841
|
+
severity: "medium",
|
|
842
|
+
blocker: false,
|
|
843
|
+
symbol: "ACTION",
|
|
844
|
+
riskClass: "S3",
|
|
845
|
+
mode: "INFERRED",
|
|
846
|
+
confidence: "low",
|
|
847
|
+
detectionMethod: "package-metadata",
|
|
848
|
+
evidence,
|
|
849
|
+
impact: "The server appears to integrate with an external system and may take actions (e.g. open PRs, send messages) on the agent's behalf.",
|
|
850
|
+
fix: "Confirm which mutating tools are exposed and require manual approval for autonomous use.",
|
|
851
|
+
falsePositiveNote: "Name-based inference; a read-only integration would not actually mutate anything."
|
|
852
|
+
}
|
|
853
|
+
];
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// ../../packages/static-analyzer/src/detectors/financialAction.ts
|
|
857
|
+
var FINANCIAL_HINTS = [
|
|
858
|
+
"stripe",
|
|
859
|
+
"paypal",
|
|
860
|
+
"braintree",
|
|
861
|
+
"adyen",
|
|
862
|
+
"payment",
|
|
863
|
+
"payments",
|
|
864
|
+
"payout",
|
|
865
|
+
"payouts",
|
|
866
|
+
"payroll",
|
|
867
|
+
"charge",
|
|
868
|
+
"invoice",
|
|
869
|
+
"invoicing",
|
|
870
|
+
"billing",
|
|
871
|
+
"refund",
|
|
872
|
+
"transfer",
|
|
873
|
+
"wire",
|
|
874
|
+
"ach",
|
|
875
|
+
"sepa",
|
|
876
|
+
"wallet",
|
|
877
|
+
"coinbase",
|
|
878
|
+
"treasury",
|
|
879
|
+
"bank",
|
|
880
|
+
"checkout"
|
|
881
|
+
];
|
|
882
|
+
var PAYMENT_ACTION_VERBS = [
|
|
883
|
+
"create_payment",
|
|
884
|
+
"create_charge",
|
|
885
|
+
"create payment",
|
|
886
|
+
"make_payment",
|
|
887
|
+
"make payment",
|
|
888
|
+
"send_payment",
|
|
889
|
+
"send money",
|
|
890
|
+
"send_money",
|
|
891
|
+
"transfer_funds",
|
|
892
|
+
"transfer funds",
|
|
893
|
+
"create_payout",
|
|
894
|
+
"create payout",
|
|
895
|
+
"create_transfer",
|
|
896
|
+
"charge_card",
|
|
897
|
+
"charge card",
|
|
898
|
+
"process_payment",
|
|
899
|
+
"process payment",
|
|
900
|
+
"issue_refund",
|
|
901
|
+
"purchase",
|
|
902
|
+
"buy_",
|
|
903
|
+
"place_order",
|
|
904
|
+
"place order",
|
|
905
|
+
"withdraw"
|
|
906
|
+
];
|
|
907
|
+
function collectHints2(text) {
|
|
908
|
+
if (!text) return [];
|
|
909
|
+
const lower = text.toLowerCase();
|
|
910
|
+
return FINANCIAL_HINTS.filter((h) => lower.includes(h));
|
|
911
|
+
}
|
|
912
|
+
function collectActionVerbs(text) {
|
|
913
|
+
if (!text) return [];
|
|
914
|
+
const lower = text.toLowerCase();
|
|
915
|
+
return PAYMENT_ACTION_VERBS.filter((v) => lower.includes(v));
|
|
916
|
+
}
|
|
917
|
+
function detectFinancialAction(ctx) {
|
|
918
|
+
const { server, binding } = ctx;
|
|
919
|
+
const findings = [];
|
|
920
|
+
const actionEvidence = [];
|
|
921
|
+
const verbs = /* @__PURE__ */ new Set();
|
|
922
|
+
for (const tool of server.providedTools) {
|
|
923
|
+
for (const v of collectActionVerbs(tool.name)) {
|
|
924
|
+
verbs.add(v);
|
|
925
|
+
actionEvidence.push({ type: "tool-metadata", key: "tool", value: tool.name });
|
|
926
|
+
}
|
|
927
|
+
for (const v of collectActionVerbs(tool.description)) {
|
|
928
|
+
verbs.add(v);
|
|
929
|
+
actionEvidence.push({
|
|
930
|
+
type: "tool-metadata",
|
|
931
|
+
key: tool.name ? `tools.${tool.name}.description` : "tools.description",
|
|
932
|
+
snippet: v
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (verbs.size > 0) {
|
|
937
|
+
const hasSecret = server.envKeys.some(
|
|
938
|
+
(k) => /TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL|AUTH/i.test(k)
|
|
939
|
+
);
|
|
940
|
+
const networkCapable = Boolean(binding.remoteUrl) || binding.runtimeExecutable;
|
|
941
|
+
if (hasSecret) {
|
|
942
|
+
actionEvidence.push({ type: "config", key: "env", value: "credential present" });
|
|
943
|
+
}
|
|
944
|
+
if (hasSecret || networkCapable) {
|
|
945
|
+
findings.push({
|
|
946
|
+
id: "action.financial-observed",
|
|
947
|
+
title: "Exposes an observed money-moving action",
|
|
948
|
+
severity: "critical",
|
|
949
|
+
blocker: true,
|
|
950
|
+
symbol: "MONEY",
|
|
951
|
+
riskClass: "S5",
|
|
952
|
+
mode: "OBSERVED",
|
|
953
|
+
confidence: "high",
|
|
954
|
+
detectionMethod: "tool-metadata",
|
|
955
|
+
evidence: actionEvidence,
|
|
956
|
+
impact: "A provided tool explicitly performs a financial action (e.g. create a payment, transfer funds, issue a refund) and the server carries credentials or network access. An autonomous agent invoking it could move money irreversibly.",
|
|
957
|
+
fix: "Require explicit human approval for this tool, enforce a hard spending boundary, and never allow autonomous invocation.",
|
|
958
|
+
falsePositiveNote: "If the tool only reads payment data (not moves money), reclassify it; this fires on money-moving verbs in the model-visible metadata."
|
|
959
|
+
});
|
|
960
|
+
return findings;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
const hints = /* @__PURE__ */ new Set();
|
|
964
|
+
const evidence = [];
|
|
965
|
+
for (const h of collectHints2(binding.packageName)) {
|
|
966
|
+
hints.add(h);
|
|
967
|
+
evidence.push({
|
|
968
|
+
type: "runtime-binding",
|
|
969
|
+
key: "package",
|
|
970
|
+
value: binding.packageName
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
for (const tool of server.providedTools) {
|
|
974
|
+
for (const h of collectHints2(tool.name)) {
|
|
975
|
+
hints.add(h);
|
|
976
|
+
evidence.push({ type: "tool-metadata", key: "tool", value: tool.name });
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
if (hints.size === 0) return findings;
|
|
980
|
+
findings.push({
|
|
981
|
+
id: "action.financial",
|
|
982
|
+
title: "May perform financial or irreversible actions",
|
|
983
|
+
severity: "high",
|
|
984
|
+
blocker: false,
|
|
985
|
+
symbol: "MONEY",
|
|
986
|
+
riskClass: "S5",
|
|
987
|
+
mode: "INFERRED",
|
|
988
|
+
confidence: "low",
|
|
989
|
+
detectionMethod: "package-metadata",
|
|
990
|
+
evidence,
|
|
991
|
+
impact: "The server appears to integrate with a payments or financial system and could move money or take irreversible actions on the agent's behalf.",
|
|
992
|
+
fix: "Confirm which financial tools are exposed, require manual approval, and never allow autonomous use without a hard spending boundary.",
|
|
993
|
+
falsePositiveNote: "Name-based inference; a read-only reporting integration would not actually move money."
|
|
994
|
+
});
|
|
995
|
+
return findings;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
// ../../packages/static-analyzer/src/analyzeServerConfig.ts
|
|
999
|
+
var DETECTORS = [
|
|
1000
|
+
detectBroadFilesystemPath,
|
|
1001
|
+
detectDangerousCommand,
|
|
1002
|
+
detectPromptPoisoning,
|
|
1003
|
+
detectSecretEnvKeys,
|
|
1004
|
+
detectUnpinnedPackage,
|
|
1005
|
+
detectUnknownRemote,
|
|
1006
|
+
detectExternalMutation,
|
|
1007
|
+
detectFinancialAction
|
|
1008
|
+
];
|
|
1009
|
+
function analyzeServerConfig(server) {
|
|
1010
|
+
const binding = resolveRuntimeBinding(server);
|
|
1011
|
+
const ctx = { server, binding };
|
|
1012
|
+
const findings = [];
|
|
1013
|
+
for (const detector of DETECTORS) {
|
|
1014
|
+
findings.push(...detector(ctx));
|
|
1015
|
+
}
|
|
1016
|
+
return findings;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// ../../packages/risk-engine/src/computeRiskClass.ts
|
|
1020
|
+
function computeRiskClass(findings, binding) {
|
|
1021
|
+
const classes = findings.map((f) => f.riskClass);
|
|
1022
|
+
if (classes.length === 0) {
|
|
1023
|
+
return binding.runtimeExecutable || binding.remoteUrl ? "S1" : "S0";
|
|
1024
|
+
}
|
|
1025
|
+
return highestRiskClass(classes);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// ../../packages/risk-engine/src/computeVerdict.ts
|
|
1029
|
+
function computeVerdict(findings, binding) {
|
|
1030
|
+
if (findings.some((f) => f.blocker)) return "BLOCK";
|
|
1031
|
+
const unverifiable = !binding.sourceKnown && (Boolean(binding.remoteUrl) || binding.runtimeExecutable);
|
|
1032
|
+
if (unverifiable) return "UNKNOWN";
|
|
1033
|
+
if (findings.some((f) => f.severity === "high" || f.severity === "critical")) {
|
|
1034
|
+
return "REVIEW";
|
|
1035
|
+
}
|
|
1036
|
+
if (findings.length > 0) return "REVIEW";
|
|
1037
|
+
return "SAFE";
|
|
1038
|
+
}
|
|
1039
|
+
function computeConfidence(findings) {
|
|
1040
|
+
if (findings.length === 0) return "high";
|
|
1041
|
+
const rank = { low: 0, medium: 1, high: 2 };
|
|
1042
|
+
let worst = "high";
|
|
1043
|
+
for (const f of findings) {
|
|
1044
|
+
if (rank[f.confidence] < rank[worst]) worst = f.confidence;
|
|
1045
|
+
}
|
|
1046
|
+
return worst;
|
|
1047
|
+
}
|
|
1048
|
+
function computeRecommendedPolicy(verdict, findings) {
|
|
1049
|
+
const hasExec = findings.some((f) => f.symbol === "EXEC" || f.symbol === "MONEY");
|
|
1050
|
+
const hasFiles = findings.some((f) => f.symbol === "FILES");
|
|
1051
|
+
if (verdict === "BLOCK") {
|
|
1052
|
+
return {
|
|
1053
|
+
autonomousUse: "deny",
|
|
1054
|
+
manualApproval: "required",
|
|
1055
|
+
sandbox: hasExec || hasFiles ? "required" : "recommended"
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
if (verdict === "UNKNOWN") {
|
|
1059
|
+
return {
|
|
1060
|
+
autonomousUse: "deny",
|
|
1061
|
+
manualApproval: "required",
|
|
1062
|
+
sandbox: "required"
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
if (verdict === "REVIEW") {
|
|
1066
|
+
return {
|
|
1067
|
+
autonomousUse: "warn",
|
|
1068
|
+
manualApproval: "recommended",
|
|
1069
|
+
sandbox: "recommended"
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
return {
|
|
1073
|
+
autonomousUse: "allow",
|
|
1074
|
+
manualApproval: "none",
|
|
1075
|
+
sandbox: "none"
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// ../../packages/risk-engine/src/computeReproducibility.ts
|
|
1080
|
+
function computeReproducibility(binding, _findings) {
|
|
1081
|
+
const reasons = [];
|
|
1082
|
+
if (binding.packageName && !binding.isVersionPinned) {
|
|
1083
|
+
reasons.push("Package version is not pinned");
|
|
1084
|
+
}
|
|
1085
|
+
if (binding.remoteUrl && !binding.sourceKnown) {
|
|
1086
|
+
reasons.push("Remote endpoint could not be verified");
|
|
1087
|
+
}
|
|
1088
|
+
if (binding.runtimeExecutable && !binding.sourceKnown && !binding.remoteUrl) {
|
|
1089
|
+
reasons.push("Runtime source could not be identified");
|
|
1090
|
+
}
|
|
1091
|
+
let level;
|
|
1092
|
+
if (reasons.length === 0) level = "HIGH";
|
|
1093
|
+
else if (reasons.length === 1) level = "MEDIUM";
|
|
1094
|
+
else level = "LOW";
|
|
1095
|
+
return { level, reasons };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// ../../packages/risk-engine/src/assessServer.ts
|
|
1099
|
+
var SEVERITY_RANK = {
|
|
1100
|
+
info: 0,
|
|
1101
|
+
low: 1,
|
|
1102
|
+
medium: 2,
|
|
1103
|
+
high: 3,
|
|
1104
|
+
critical: 4
|
|
1105
|
+
};
|
|
1106
|
+
function uniqueSymbols(findings) {
|
|
1107
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1108
|
+
const out = [];
|
|
1109
|
+
for (const f of findings) {
|
|
1110
|
+
if (!seen.has(f.symbol)) {
|
|
1111
|
+
seen.add(f.symbol);
|
|
1112
|
+
out.push(f.symbol);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return out;
|
|
1116
|
+
}
|
|
1117
|
+
function rankFindings(findings) {
|
|
1118
|
+
return [...findings].sort((a, b) => {
|
|
1119
|
+
if (a.blocker !== b.blocker) return a.blocker ? -1 : 1;
|
|
1120
|
+
return SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
function assessServer(findings, binding) {
|
|
1124
|
+
const verdict = computeVerdict(findings, binding);
|
|
1125
|
+
const riskClass = computeRiskClass(findings, binding);
|
|
1126
|
+
const symbols = uniqueSymbols(rankFindings(findings));
|
|
1127
|
+
const confidence = computeConfidence(findings);
|
|
1128
|
+
const reproducibility = computeReproducibility(binding, findings);
|
|
1129
|
+
const policy = computeRecommendedPolicy(verdict, findings);
|
|
1130
|
+
return {
|
|
1131
|
+
verdict,
|
|
1132
|
+
riskClass,
|
|
1133
|
+
symbols,
|
|
1134
|
+
confidence,
|
|
1135
|
+
reproducibility,
|
|
1136
|
+
policy,
|
|
1137
|
+
observed: findings.filter((f) => f.mode === "OBSERVED"),
|
|
1138
|
+
inferred: findings.filter((f) => f.mode === "INFERRED"),
|
|
1139
|
+
topFindings: rankFindings(findings).slice(0, 3)
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// ../../packages/fingerprint/src/hashJson.ts
|
|
1144
|
+
import { createHash } from "node:crypto";
|
|
1145
|
+
function sha256(input) {
|
|
1146
|
+
return "sha256:" + createHash("sha256").update(input, "utf8").digest("hex");
|
|
1147
|
+
}
|
|
1148
|
+
function stableStringify(value) {
|
|
1149
|
+
return JSON.stringify(sortValue(value));
|
|
1150
|
+
}
|
|
1151
|
+
function sortValue(value) {
|
|
1152
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
1153
|
+
if (value && typeof value === "object") {
|
|
1154
|
+
const out = {};
|
|
1155
|
+
for (const key of Object.keys(value).sort()) {
|
|
1156
|
+
out[key] = sortValue(value[key]);
|
|
1157
|
+
}
|
|
1158
|
+
return out;
|
|
1159
|
+
}
|
|
1160
|
+
return value;
|
|
1161
|
+
}
|
|
1162
|
+
function hashJson(value) {
|
|
1163
|
+
return sha256(stableStringify(value));
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// ../../packages/fingerprint/src/computeFingerprints.ts
|
|
1167
|
+
function computeFingerprints(input) {
|
|
1168
|
+
const { server, binding, symbols, findingIds } = input;
|
|
1169
|
+
const targetSpec = {
|
|
1170
|
+
command: binding.declaredCommand,
|
|
1171
|
+
args: binding.declaredArgs,
|
|
1172
|
+
envKeys: [...server.envKeys].sort(),
|
|
1173
|
+
remoteUrl: binding.remoteUrl
|
|
1174
|
+
};
|
|
1175
|
+
const packageSpec = binding.packageName !== void 0 ? `${binding.packageName}@${binding.packageVersionSpec ?? ""}` : void 0;
|
|
1176
|
+
const riskSurface = {
|
|
1177
|
+
symbols: [...symbols].sort(),
|
|
1178
|
+
findingIds: [...findingIds].sort()
|
|
1179
|
+
};
|
|
1180
|
+
const sourceText = server.instructions ?? (server.providedTools.length > 0 ? JSON.stringify(server.providedTools) : void 0);
|
|
1181
|
+
const toolMetadata = server.providedTools.length > 0 ? server.providedTools : void 0;
|
|
1182
|
+
const fp = {
|
|
1183
|
+
configHash: hashJson(server.raw),
|
|
1184
|
+
targetSpecHash: hashJson(targetSpec),
|
|
1185
|
+
riskSurfaceHash: hashJson(riskSurface)
|
|
1186
|
+
};
|
|
1187
|
+
if (packageSpec !== void 0) fp.packageSpecHash = sha256(packageSpec);
|
|
1188
|
+
if (sourceText !== void 0) fp.sourceHash = sha256(sourceText);
|
|
1189
|
+
if (toolMetadata !== void 0) fp.toolMetadataHash = hashJson(toolMetadata);
|
|
1190
|
+
return fp;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
// ../../packages/core/src/summarize.ts
|
|
1194
|
+
function summarize(name, verdict, a, policyApplied) {
|
|
1195
|
+
const symbolText = a.symbols.length > 0 ? a.symbols.map((s) => RISK_SYMBOL_LABEL[s]).join(", ") : "no risk surface observed";
|
|
1196
|
+
const cls = `${a.riskClass} ${RISK_CLASS_LABEL[a.riskClass]}`;
|
|
1197
|
+
switch (verdict) {
|
|
1198
|
+
case "BLOCK":
|
|
1199
|
+
return policyApplied ? `"${name}" would be blocked but was downgraded by policy. Risk: ${symbolText} (${cls}).` : `"${name}" is blocked. Risk: ${symbolText} (${cls}).`;
|
|
1200
|
+
case "UNKNOWN":
|
|
1201
|
+
return `"${name}" could not be verified (insufficient evidence). Risk: ${symbolText} (${cls}).`;
|
|
1202
|
+
case "REVIEW":
|
|
1203
|
+
return `"${name}" needs review. Risk: ${symbolText} (${cls}).`;
|
|
1204
|
+
case "SAFE":
|
|
1205
|
+
return `"${name}" has no blockers observed (${cls}).`;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// ../../packages/core/src/scanServer.ts
|
|
1210
|
+
function scanServer(input, opts) {
|
|
1211
|
+
const { policy, now, generatedAt, extraFindings } = resolveScanOptions(opts);
|
|
1212
|
+
const { server } = input;
|
|
1213
|
+
const binding = resolveRuntimeBinding(server);
|
|
1214
|
+
const staticFindings = analyzeServerConfig(server);
|
|
1215
|
+
const injected = extraFindings[server.name] ?? [];
|
|
1216
|
+
const findings = [...staticFindings, ...injected];
|
|
1217
|
+
const assessment = assessServer(findings, binding);
|
|
1218
|
+
if (injected.length > 0) {
|
|
1219
|
+
const offlineVerdict = assessServer(staticFindings, binding).verdict;
|
|
1220
|
+
if (VERDICT_SEVERITY[assessment.verdict] < VERDICT_SEVERITY[offlineVerdict]) {
|
|
1221
|
+
throw new Error(
|
|
1222
|
+
`Online enrichment downgraded verdict for "${server.name}" (${offlineVerdict} -> ${assessment.verdict}); enrichment must never lower risk.`
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const decision = applyPolicy(
|
|
1227
|
+
assessment.verdict,
|
|
1228
|
+
server.name,
|
|
1229
|
+
findings,
|
|
1230
|
+
policy,
|
|
1231
|
+
now
|
|
1232
|
+
);
|
|
1233
|
+
const verdict = decision.verdict;
|
|
1234
|
+
const fingerprints = computeFingerprints({
|
|
1235
|
+
server,
|
|
1236
|
+
binding,
|
|
1237
|
+
symbols: assessment.symbols,
|
|
1238
|
+
findingIds: findings.map((f) => f.id)
|
|
1239
|
+
});
|
|
1240
|
+
const diagnostics = [];
|
|
1241
|
+
if (decision.changed && decision.note) {
|
|
1242
|
+
diagnostics.push({ level: "info", code: "policy.applied", message: decision.note });
|
|
1243
|
+
}
|
|
1244
|
+
const target = {
|
|
1245
|
+
name: server.name,
|
|
1246
|
+
kind: input.targetKind ?? "cursor-mcp-config",
|
|
1247
|
+
source: binding.packageName ?? binding.remoteUrl,
|
|
1248
|
+
version: binding.packageVersionSpec,
|
|
1249
|
+
configPath: server.sourceConfigPath
|
|
1250
|
+
};
|
|
1251
|
+
return {
|
|
1252
|
+
schemaVersion: "calllint.report.v0",
|
|
1253
|
+
reportKind: "single-target",
|
|
1254
|
+
target,
|
|
1255
|
+
verdict,
|
|
1256
|
+
publicVerdictLabel: VERDICT_PUBLIC_LABEL[verdict],
|
|
1257
|
+
policyApplied: decision.changed,
|
|
1258
|
+
riskClass: assessment.riskClass,
|
|
1259
|
+
symbols: assessment.symbols,
|
|
1260
|
+
confidence: assessment.confidence,
|
|
1261
|
+
reproducibility: assessment.reproducibility,
|
|
1262
|
+
summary: summarize(server.name, verdict, assessment, decision.changed),
|
|
1263
|
+
observed: assessment.observed,
|
|
1264
|
+
inferred: assessment.inferred,
|
|
1265
|
+
findings,
|
|
1266
|
+
topFindings: assessment.topFindings,
|
|
1267
|
+
policy: assessment.policy,
|
|
1268
|
+
fingerprints,
|
|
1269
|
+
diagnostics,
|
|
1270
|
+
generatedAt
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// ../../packages/config-parser/src/parseJsonFile.ts
|
|
1275
|
+
var ConfigParseError = class extends Error {
|
|
1276
|
+
constructor(message, path) {
|
|
1277
|
+
super(message);
|
|
1278
|
+
this.path = path;
|
|
1279
|
+
this.name = "ConfigParseError";
|
|
1280
|
+
}
|
|
1281
|
+
code = "config.parse-error";
|
|
1282
|
+
};
|
|
1283
|
+
function parseJsonText(text, path) {
|
|
1284
|
+
try {
|
|
1285
|
+
return JSON.parse(text);
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
1288
|
+
throw new ConfigParseError(`Invalid JSON: ${reason}`, path);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// ../../packages/config-parser/src/normalizeMcpServers.ts
|
|
1293
|
+
function isRecord2(v) {
|
|
1294
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1295
|
+
}
|
|
1296
|
+
function asString(v) {
|
|
1297
|
+
return typeof v === "string" ? v : void 0;
|
|
1298
|
+
}
|
|
1299
|
+
function asStringArray(v) {
|
|
1300
|
+
if (!Array.isArray(v)) return [];
|
|
1301
|
+
return v.filter((x) => typeof x === "string");
|
|
1302
|
+
}
|
|
1303
|
+
function transportFor(server) {
|
|
1304
|
+
if (asString(server.url)) {
|
|
1305
|
+
const type = asString(server.type);
|
|
1306
|
+
if (type === "sse") return "sse";
|
|
1307
|
+
if (type === "http" || type === "streamable-http") return "http";
|
|
1308
|
+
return "http";
|
|
1309
|
+
}
|
|
1310
|
+
if (asString(server.command)) return "stdio";
|
|
1311
|
+
return "unknown";
|
|
1312
|
+
}
|
|
1313
|
+
function extractProvidedTools(server) {
|
|
1314
|
+
const guard = server["x-calllint"];
|
|
1315
|
+
if (!isRecord2(guard)) return [];
|
|
1316
|
+
const tools = guard.tools;
|
|
1317
|
+
if (!Array.isArray(tools)) return [];
|
|
1318
|
+
const out = [];
|
|
1319
|
+
for (const t of tools) {
|
|
1320
|
+
if (!isRecord2(t)) continue;
|
|
1321
|
+
out.push({
|
|
1322
|
+
name: asString(t.name),
|
|
1323
|
+
description: asString(t.description),
|
|
1324
|
+
inputSchemaText: asString(t.inputSchemaText)
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
return out;
|
|
1328
|
+
}
|
|
1329
|
+
function extractInstructions(server) {
|
|
1330
|
+
const guard = server["x-calllint"];
|
|
1331
|
+
if (isRecord2(guard) && asString(guard.instructions)) {
|
|
1332
|
+
return asString(guard.instructions);
|
|
1333
|
+
}
|
|
1334
|
+
return asString(server.instructions);
|
|
1335
|
+
}
|
|
1336
|
+
function findServerMap(root) {
|
|
1337
|
+
if (!isRecord2(root)) return {};
|
|
1338
|
+
if (isRecord2(root.mcpServers)) return root.mcpServers;
|
|
1339
|
+
if (isRecord2(root.servers)) return root.servers;
|
|
1340
|
+
const entries = Object.entries(root);
|
|
1341
|
+
if (entries.length > 0 && entries.every(
|
|
1342
|
+
([, v]) => isRecord2(v) && ("command" in v || "url" in v)
|
|
1343
|
+
)) {
|
|
1344
|
+
return root;
|
|
1345
|
+
}
|
|
1346
|
+
return {};
|
|
1347
|
+
}
|
|
1348
|
+
function normalizeMcpServers(root, sourceConfigPath) {
|
|
1349
|
+
const map = findServerMap(root);
|
|
1350
|
+
const servers = [];
|
|
1351
|
+
for (const [name, value] of Object.entries(map)) {
|
|
1352
|
+
const server = isRecord2(value) ? value : {};
|
|
1353
|
+
const envRaw = isRecord2(server.env) ? server.env : {};
|
|
1354
|
+
const env = {};
|
|
1355
|
+
for (const [k, v] of Object.entries(envRaw)) {
|
|
1356
|
+
env[k] = typeof v === "string" ? v : String(v);
|
|
1357
|
+
}
|
|
1358
|
+
servers.push({
|
|
1359
|
+
name,
|
|
1360
|
+
sourceConfigPath,
|
|
1361
|
+
transport: transportFor(server),
|
|
1362
|
+
command: asString(server.command),
|
|
1363
|
+
args: asStringArray(server.args),
|
|
1364
|
+
envKeys: Object.keys(env),
|
|
1365
|
+
env,
|
|
1366
|
+
url: asString(server.url),
|
|
1367
|
+
instructions: extractInstructions(server),
|
|
1368
|
+
providedTools: extractProvidedTools(server),
|
|
1369
|
+
raw: value
|
|
1370
|
+
});
|
|
1371
|
+
}
|
|
1372
|
+
return servers;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// ../../packages/config-parser/src/parseConfig.ts
|
|
1376
|
+
import { basename } from "node:path";
|
|
1377
|
+
function kindForPath(path) {
|
|
1378
|
+
const base = basename(path).toLowerCase();
|
|
1379
|
+
if (base.includes("settings")) return "claude-settings";
|
|
1380
|
+
if (base === "mcp.json" || path.includes(".cursor")) return "cursor-mcp-config";
|
|
1381
|
+
return "cursor-mcp-config";
|
|
1382
|
+
}
|
|
1383
|
+
function parseConfigText(text, configPath = "<inline>") {
|
|
1384
|
+
const root = parseJsonText(text, configPath);
|
|
1385
|
+
return {
|
|
1386
|
+
configPath,
|
|
1387
|
+
kind: configPath === "<inline>" ? "inline" : kindForPath(configPath),
|
|
1388
|
+
servers: normalizeMcpServers(root, configPath),
|
|
1389
|
+
root
|
|
1390
|
+
};
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// ../../packages/core/src/scanConfig.ts
|
|
1394
|
+
function aggregate(configPath, reports, generatedAt) {
|
|
1395
|
+
const counts = { SAFE: 0, REVIEW: 0, BLOCK: 0, UNKNOWN: 0 };
|
|
1396
|
+
for (const r of reports) counts[r.verdict]++;
|
|
1397
|
+
const verdict = mostSevereVerdict(reports.map((r) => r.verdict));
|
|
1398
|
+
return {
|
|
1399
|
+
schemaVersion: "calllint.report.v0",
|
|
1400
|
+
reportKind: "config-summary",
|
|
1401
|
+
configPath,
|
|
1402
|
+
verdict,
|
|
1403
|
+
publicVerdictLabel: VERDICT_PUBLIC_LABEL[verdict],
|
|
1404
|
+
counts,
|
|
1405
|
+
reports,
|
|
1406
|
+
diagnostics: [],
|
|
1407
|
+
generatedAt
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1410
|
+
function scanParsed(parsed, opts) {
|
|
1411
|
+
const { generatedAt } = resolveScanOptions(opts);
|
|
1412
|
+
const reports = parsed.servers.map(
|
|
1413
|
+
(server) => scanServer({ server, targetKind: parsed.kind }, opts)
|
|
1414
|
+
);
|
|
1415
|
+
return aggregate(parsed.configPath, reports, generatedAt);
|
|
1416
|
+
}
|
|
1417
|
+
function scanConfigText(text, configPath, opts) {
|
|
1418
|
+
return scanParsed(parseConfigText(text, configPath), opts);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// ../../packages/core/src/drift.ts
|
|
1422
|
+
function entryFromReport(report) {
|
|
1423
|
+
return {
|
|
1424
|
+
server: report.target.name,
|
|
1425
|
+
verdict: report.verdict,
|
|
1426
|
+
symbols: [...report.symbols],
|
|
1427
|
+
findingIds: report.findings.map((f) => f.id).sort(),
|
|
1428
|
+
fingerprints: report.fingerprints
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
function buildBaseline(summary, createdAt) {
|
|
1432
|
+
return {
|
|
1433
|
+
schemaVersion: "calllint.baseline.v0",
|
|
1434
|
+
configPath: summary.configPath,
|
|
1435
|
+
entries: summary.reports.map(entryFromReport),
|
|
1436
|
+
createdAt
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
function sameStringSet(a, b) {
|
|
1440
|
+
if (a.length !== b.length) return false;
|
|
1441
|
+
const sortedA = [...a].sort();
|
|
1442
|
+
const sortedB = [...b].sort();
|
|
1443
|
+
return sortedA.every((v, i) => v === sortedB[i]);
|
|
1444
|
+
}
|
|
1445
|
+
function diffEntry(base, cur) {
|
|
1446
|
+
const reasons = [];
|
|
1447
|
+
let rugPull = false;
|
|
1448
|
+
const fpBase = base.fingerprints;
|
|
1449
|
+
const fpCur = cur.fingerprints;
|
|
1450
|
+
if (fpBase.packageSpecHash !== fpCur.packageSpecHash) {
|
|
1451
|
+
reasons.push("package spec changed (rug-pull signal)");
|
|
1452
|
+
rugPull = true;
|
|
1453
|
+
}
|
|
1454
|
+
if (fpBase.sourceHash !== fpCur.sourceHash) {
|
|
1455
|
+
reasons.push("source text changed");
|
|
1456
|
+
rugPull = true;
|
|
1457
|
+
}
|
|
1458
|
+
if (fpBase.toolMetadataHash !== fpCur.toolMetadataHash) {
|
|
1459
|
+
reasons.push("tool metadata changed");
|
|
1460
|
+
rugPull = true;
|
|
1461
|
+
}
|
|
1462
|
+
if (fpBase.riskSurfaceHash !== fpCur.riskSurfaceHash) {
|
|
1463
|
+
reasons.push("risk surface changed");
|
|
1464
|
+
}
|
|
1465
|
+
if (fpBase.configHash !== fpCur.configHash) {
|
|
1466
|
+
reasons.push("config changed");
|
|
1467
|
+
}
|
|
1468
|
+
if (base.verdict !== cur.verdict) {
|
|
1469
|
+
reasons.push(`verdict ${base.verdict} -> ${cur.verdict}`);
|
|
1470
|
+
}
|
|
1471
|
+
if (!sameStringSet(base.findingIds, cur.findingIds)) {
|
|
1472
|
+
reasons.push("finding set changed");
|
|
1473
|
+
}
|
|
1474
|
+
let status = "unchanged";
|
|
1475
|
+
if (rugPull) status = "package-changed";
|
|
1476
|
+
else if (base.verdict !== cur.verdict) status = "verdict-changed";
|
|
1477
|
+
else if (fpBase.riskSurfaceHash !== fpCur.riskSurfaceHash) status = "risk-surface-changed";
|
|
1478
|
+
else if (fpBase.configHash !== fpCur.configHash) status = "config-changed";
|
|
1479
|
+
return {
|
|
1480
|
+
server: base.server,
|
|
1481
|
+
status,
|
|
1482
|
+
reasons,
|
|
1483
|
+
baselineVerdict: base.verdict,
|
|
1484
|
+
currentVerdict: cur.verdict,
|
|
1485
|
+
rugPull
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
function computeDrift(baseline, summary, generatedAt) {
|
|
1489
|
+
const current = /* @__PURE__ */ new Map();
|
|
1490
|
+
for (const r of summary.reports) current.set(r.target.name, entryFromReport(r));
|
|
1491
|
+
const baseByName = /* @__PURE__ */ new Map();
|
|
1492
|
+
for (const e of baseline.entries) baseByName.set(e.server, e);
|
|
1493
|
+
const entries = [];
|
|
1494
|
+
for (const base of baseline.entries) {
|
|
1495
|
+
const cur = current.get(base.server);
|
|
1496
|
+
if (!cur) {
|
|
1497
|
+
entries.push({
|
|
1498
|
+
server: base.server,
|
|
1499
|
+
status: "removed",
|
|
1500
|
+
reasons: ["server removed from config"],
|
|
1501
|
+
baselineVerdict: base.verdict,
|
|
1502
|
+
rugPull: false
|
|
1503
|
+
});
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
entries.push(diffEntry(base, cur));
|
|
1507
|
+
}
|
|
1508
|
+
for (const [name, cur] of current) {
|
|
1509
|
+
if (!baseByName.has(name)) {
|
|
1510
|
+
entries.push({
|
|
1511
|
+
server: name,
|
|
1512
|
+
status: "added",
|
|
1513
|
+
reasons: ["new server not in baseline"],
|
|
1514
|
+
currentVerdict: cur.verdict,
|
|
1515
|
+
rugPull: false
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
const drifted = entries.some((e) => e.status !== "unchanged");
|
|
1520
|
+
const rugPullDetected = entries.some((e) => e.rugPull);
|
|
1521
|
+
return {
|
|
1522
|
+
schemaVersion: "calllint.drift.v0",
|
|
1523
|
+
configPath: summary.configPath,
|
|
1524
|
+
drifted,
|
|
1525
|
+
rugPullDetected,
|
|
1526
|
+
entries,
|
|
1527
|
+
generatedAt
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
// ../../packages/core/src/targets.ts
|
|
1532
|
+
function parseTargetSpec(arg) {
|
|
1533
|
+
if (arg.startsWith("npm:")) {
|
|
1534
|
+
return { kind: "npm", raw: arg, packageSpec: arg.slice("npm:".length) };
|
|
1535
|
+
}
|
|
1536
|
+
if (arg.startsWith("github:")) {
|
|
1537
|
+
const rest = arg.slice("github:".length);
|
|
1538
|
+
const at = rest.lastIndexOf("@");
|
|
1539
|
+
const slash = rest.indexOf("/");
|
|
1540
|
+
if (at > slash && at !== -1) {
|
|
1541
|
+
return { kind: "github", raw: arg, repo: rest.slice(0, at), ref: rest.slice(at + 1) };
|
|
1542
|
+
}
|
|
1543
|
+
return { kind: "github", raw: arg, repo: rest };
|
|
1544
|
+
}
|
|
1545
|
+
return { kind: "path", raw: arg };
|
|
1546
|
+
}
|
|
1547
|
+
function serverNameForPackage(packageSpec) {
|
|
1548
|
+
const noVersion = packageSpec.startsWith("@") ? packageSpec.replace(/(@[^/]+\/[^@]+)@.*/, "$1") : packageSpec.split("@")[0];
|
|
1549
|
+
return noVersion.replace(/^@/, "").replace(/\//g, "-");
|
|
1550
|
+
}
|
|
1551
|
+
function synthesizeNpmConfig(packageSpec) {
|
|
1552
|
+
const name = serverNameForPackage(packageSpec);
|
|
1553
|
+
const config = {
|
|
1554
|
+
mcpServers: {
|
|
1555
|
+
[name]: {
|
|
1556
|
+
command: "npx",
|
|
1557
|
+
args: ["-y", packageSpec]
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
};
|
|
1561
|
+
return { text: JSON.stringify(config), configPath: `npm:${packageSpec}` };
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
// ../../packages/core/src/cache.ts
|
|
1565
|
+
import { writeFileSync, readFileSync as readFileSync2, mkdirSync, existsSync } from "node:fs";
|
|
1566
|
+
import { dirname, join } from "node:path";
|
|
1567
|
+
function defaultCachePath(cwd = process.cwd()) {
|
|
1568
|
+
return join(cwd, ".calllint", "last-scan.json");
|
|
1569
|
+
}
|
|
1570
|
+
function writeCache(report, path = defaultCachePath()) {
|
|
1571
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1572
|
+
writeFileSync(path, JSON.stringify(report, null, 2), "utf8");
|
|
1573
|
+
}
|
|
1574
|
+
function readCache(path = defaultCachePath()) {
|
|
1575
|
+
if (!existsSync(path)) return void 0;
|
|
1576
|
+
try {
|
|
1577
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
1578
|
+
} catch {
|
|
1579
|
+
return void 0;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function defaultBaselinePath(cwd = process.cwd()) {
|
|
1583
|
+
return join(cwd, ".calllint", "baseline.json");
|
|
1584
|
+
}
|
|
1585
|
+
function writeBaseline(baseline, path = defaultBaselinePath()) {
|
|
1586
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
1587
|
+
writeFileSync(path, JSON.stringify(baseline, null, 2), "utf8");
|
|
1588
|
+
}
|
|
1589
|
+
function readBaseline(path = defaultBaselinePath()) {
|
|
1590
|
+
if (!existsSync(path)) return void 0;
|
|
1591
|
+
try {
|
|
1592
|
+
return JSON.parse(readFileSync2(path, "utf8"));
|
|
1593
|
+
} catch {
|
|
1594
|
+
return void 0;
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// ../../packages/report-renderer/src/style.ts
|
|
1599
|
+
var DEFAULT_STYLE = { emoji: true };
|
|
1600
|
+
var NO_EMOJI_STYLE = { emoji: false };
|
|
1601
|
+
function verdictTag(verdict, style) {
|
|
1602
|
+
return style.emoji ? VERDICT_CLI_SYMBOL[verdict] : VERDICT_TEXT_SYMBOL[verdict];
|
|
1603
|
+
}
|
|
1604
|
+
function symbolTag(symbol, style) {
|
|
1605
|
+
return style.emoji ? `${RISK_SYMBOL_EMOJI[symbol]} ${symbol}` : symbol;
|
|
1606
|
+
}
|
|
1607
|
+
function symbolList(symbols, style) {
|
|
1608
|
+
if (symbols.length === 0) return "\u2014";
|
|
1609
|
+
return symbols.map((s) => symbolTag(s, style)).join(" ");
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// ../../packages/report-renderer/src/renderJson.ts
|
|
1613
|
+
function renderJson(summary) {
|
|
1614
|
+
return JSON.stringify(summary, null, 2);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// ../../packages/report-renderer/src/renderTerminal.ts
|
|
1618
|
+
function renderFindingLine(f) {
|
|
1619
|
+
const lines = [];
|
|
1620
|
+
const flag = f.blocker ? "[BLOCKER] " : "";
|
|
1621
|
+
lines.push(` \u2022 ${flag}${f.title} (${f.id}, ${f.mode.toLowerCase()}, confidence ${f.confidence})`);
|
|
1622
|
+
if (f.evidence.length > 0) {
|
|
1623
|
+
const e = f.evidence[0];
|
|
1624
|
+
const ev = e.value ?? e.snippet ?? e.key ?? e.path ?? "";
|
|
1625
|
+
lines.push(` evidence: ${e.key ?? e.type}${ev ? ` = ${ev}` : ""}`);
|
|
1626
|
+
}
|
|
1627
|
+
lines.push(` impact: ${f.impact}`);
|
|
1628
|
+
lines.push(` fix: ${f.fix}`);
|
|
1629
|
+
return lines;
|
|
1630
|
+
}
|
|
1631
|
+
function renderServer(r, style) {
|
|
1632
|
+
const lines = [];
|
|
1633
|
+
lines.push("");
|
|
1634
|
+
lines.push(`${verdictTag(r.verdict, style)} ${r.target.name} ${symbolList(r.symbols, style)}`);
|
|
1635
|
+
lines.push(` ${r.riskClass} ${RISK_CLASS_LABEL[r.riskClass]} \xB7 reproducibility ${r.reproducibility.level} \xB7 confidence ${r.confidence}`);
|
|
1636
|
+
lines.push(` ${r.summary}`);
|
|
1637
|
+
if (r.policyApplied) {
|
|
1638
|
+
const note = r.diagnostics.find((d) => d.code === "policy.applied");
|
|
1639
|
+
if (note) lines.push(` \u2691 ${note.message}`);
|
|
1640
|
+
}
|
|
1641
|
+
const top = r.topFindings;
|
|
1642
|
+
if (top.length > 0) {
|
|
1643
|
+
lines.push("");
|
|
1644
|
+
for (const f of top) lines.push(...renderFindingLine(f));
|
|
1645
|
+
}
|
|
1646
|
+
if (r.reproducibility.reasons.length > 0) {
|
|
1647
|
+
lines.push("");
|
|
1648
|
+
lines.push(` reproducibility notes: ${r.reproducibility.reasons.join("; ")}`);
|
|
1649
|
+
}
|
|
1650
|
+
lines.push("");
|
|
1651
|
+
lines.push(` autonomous use: ${r.policy.autonomousUse} \xB7 manual approval: ${r.policy.manualApproval} \xB7 sandbox: ${r.policy.sandbox}`);
|
|
1652
|
+
return lines;
|
|
1653
|
+
}
|
|
1654
|
+
function renderTerminal(summary, style = DEFAULT_STYLE) {
|
|
1655
|
+
const lines = [];
|
|
1656
|
+
lines.push("CallLint scan");
|
|
1657
|
+
lines.push(`config: ${summary.configPath}`);
|
|
1658
|
+
lines.push(
|
|
1659
|
+
`result: ${verdictTag(summary.verdict, style)} (BLOCK ${summary.counts.BLOCK} \xB7 UNKNOWN ${summary.counts.UNKNOWN} \xB7 REVIEW ${summary.counts.REVIEW} \xB7 SAFE ${summary.counts.SAFE})`
|
|
1660
|
+
);
|
|
1661
|
+
lines.push("\u2500".repeat(60));
|
|
1662
|
+
for (const r of summary.reports) lines.push(...renderServer(r, style));
|
|
1663
|
+
return lines.join("\n");
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
// ../../packages/report-renderer/src/renderCompact.ts
|
|
1667
|
+
function renderCompact(summary, style = DEFAULT_STYLE) {
|
|
1668
|
+
const lines = [];
|
|
1669
|
+
for (const r of summary.reports) {
|
|
1670
|
+
lines.push(
|
|
1671
|
+
`${verdictTag(r.verdict, style)} ${r.target.name} ${symbolList(r.symbols, style)} ${r.riskClass}`
|
|
1672
|
+
);
|
|
1673
|
+
}
|
|
1674
|
+
lines.push(
|
|
1675
|
+
`${verdictTag(summary.verdict, style)} TOTAL BLOCK ${summary.counts.BLOCK} \xB7 UNKNOWN ${summary.counts.UNKNOWN} \xB7 REVIEW ${summary.counts.REVIEW} \xB7 SAFE ${summary.counts.SAFE}`
|
|
1676
|
+
);
|
|
1677
|
+
return lines.join("\n");
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// ../../packages/report-renderer/src/renderExplain.ts
|
|
1681
|
+
function renderFindingDetail(f, n) {
|
|
1682
|
+
const lines = [];
|
|
1683
|
+
lines.push(`${n}. ${f.title} ${f.blocker ? "[BLOCKER]" : ""}`);
|
|
1684
|
+
lines.push(` id: ${f.id}`);
|
|
1685
|
+
lines.push(` symbol: ${RISK_SYMBOL_LABEL[f.symbol]} (${f.symbol})`);
|
|
1686
|
+
lines.push(` class: ${f.riskClass} ${RISK_CLASS_LABEL[f.riskClass]}`);
|
|
1687
|
+
lines.push(` severity: ${f.severity}`);
|
|
1688
|
+
lines.push(` mode: ${f.mode} confidence: ${f.confidence}`);
|
|
1689
|
+
lines.push(` method: ${f.detectionMethod}`);
|
|
1690
|
+
if (f.evidence.length > 0) {
|
|
1691
|
+
lines.push(` evidence:`);
|
|
1692
|
+
for (const e of f.evidence) {
|
|
1693
|
+
const loc = [e.path, e.line ? `:${e.line}` : ""].filter(Boolean).join("");
|
|
1694
|
+
const detail = e.value ?? e.snippet ?? "";
|
|
1695
|
+
lines.push(` - [${e.type}] ${e.key ?? ""}${detail ? ` = ${detail}` : ""}${loc ? ` (${loc})` : ""}`);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
lines.push(` impact: ${f.impact}`);
|
|
1699
|
+
lines.push(` fix: ${f.fix}`);
|
|
1700
|
+
if (f.falsePositiveNote) lines.push(` note: ${f.falsePositiveNote}`);
|
|
1701
|
+
return lines;
|
|
1702
|
+
}
|
|
1703
|
+
function renderExplain(r, style = DEFAULT_STYLE) {
|
|
1704
|
+
const lines = [];
|
|
1705
|
+
lines.push(`${verdictTag(r.verdict, style)} ${r.target.name}`);
|
|
1706
|
+
lines.push(`label: ${r.publicVerdictLabel}`);
|
|
1707
|
+
lines.push(`source: ${r.target.source ?? "\u2014"}${r.target.version ? `@${r.target.version}` : ""}`);
|
|
1708
|
+
lines.push(`class: ${r.riskClass} ${RISK_CLASS_LABEL[r.riskClass]}`);
|
|
1709
|
+
lines.push(`symbols: ${r.symbols.length ? r.symbols.map((s) => RISK_SYMBOL_LABEL[s]).join(", ") : "none"}`);
|
|
1710
|
+
lines.push(`repro: ${r.reproducibility.level}${r.reproducibility.reasons.length ? ` (${r.reproducibility.reasons.join("; ")})` : ""}`);
|
|
1711
|
+
lines.push("");
|
|
1712
|
+
lines.push(r.summary);
|
|
1713
|
+
if (r.findings.length > 0) {
|
|
1714
|
+
lines.push("");
|
|
1715
|
+
lines.push("Findings");
|
|
1716
|
+
lines.push("\u2500".repeat(60));
|
|
1717
|
+
r.findings.forEach((f, i) => {
|
|
1718
|
+
lines.push(...renderFindingDetail(f, i + 1));
|
|
1719
|
+
lines.push("");
|
|
1720
|
+
});
|
|
1721
|
+
} else {
|
|
1722
|
+
lines.push("");
|
|
1723
|
+
lines.push("No findings.");
|
|
1724
|
+
}
|
|
1725
|
+
lines.push("Recommended policy");
|
|
1726
|
+
lines.push("\u2500".repeat(60));
|
|
1727
|
+
lines.push(`autonomous use: ${r.policy.autonomousUse}`);
|
|
1728
|
+
lines.push(`manual approval: ${r.policy.manualApproval}`);
|
|
1729
|
+
lines.push(`sandbox: ${r.policy.sandbox}`);
|
|
1730
|
+
lines.push("");
|
|
1731
|
+
lines.push("Fingerprints");
|
|
1732
|
+
lines.push("\u2500".repeat(60));
|
|
1733
|
+
lines.push(`config: ${r.fingerprints.configHash}`);
|
|
1734
|
+
lines.push(`target spec: ${r.fingerprints.targetSpecHash}`);
|
|
1735
|
+
lines.push(`risk surface:${r.fingerprints.riskSurfaceHash}`);
|
|
1736
|
+
if (r.fingerprints.packageSpecHash) lines.push(`package: ${r.fingerprints.packageSpecHash}`);
|
|
1737
|
+
return lines.join("\n");
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// ../../packages/report-renderer/src/renderDrift.ts
|
|
1741
|
+
function renderDriftJson(report) {
|
|
1742
|
+
return JSON.stringify(report, null, 2);
|
|
1743
|
+
}
|
|
1744
|
+
var STATUS_TAG = {
|
|
1745
|
+
unchanged: "OK ",
|
|
1746
|
+
"config-changed": "CFG ",
|
|
1747
|
+
"risk-surface-changed": "RISK ",
|
|
1748
|
+
"verdict-changed": "VRDCT",
|
|
1749
|
+
"package-changed": "RUG! ",
|
|
1750
|
+
added: "ADD ",
|
|
1751
|
+
removed: "DEL "
|
|
1752
|
+
};
|
|
1753
|
+
function renderDrift(report) {
|
|
1754
|
+
const lines = [];
|
|
1755
|
+
lines.push("CallLint verify (drift vs baseline)");
|
|
1756
|
+
lines.push(`config: ${report.configPath}`);
|
|
1757
|
+
const headline = report.rugPullDetected ? "RUG-PULL SIGNAL \u2014 package/source changed since baseline" : report.drifted ? "DRIFT \u2014 risk surface changed since baseline" : "no drift \u2014 matches baseline";
|
|
1758
|
+
lines.push(`result: ${headline}`);
|
|
1759
|
+
lines.push("\u2500".repeat(60));
|
|
1760
|
+
for (const e of report.entries) {
|
|
1761
|
+
const tag = STATUS_TAG[e.status] ?? e.status;
|
|
1762
|
+
lines.push(`${tag} ${e.server}`);
|
|
1763
|
+
for (const reason of e.reasons) {
|
|
1764
|
+
lines.push(` \u2022 ${reason}`);
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
return lines.join("\n");
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// ../../packages/report-renderer/src/renderSarif.ts
|
|
1771
|
+
function levelForSeverity(sev) {
|
|
1772
|
+
if (sev === "critical" || sev === "high") return "error";
|
|
1773
|
+
if (sev === "medium") return "warning";
|
|
1774
|
+
return "note";
|
|
1775
|
+
}
|
|
1776
|
+
function toUri(path) {
|
|
1777
|
+
if (path === "<stdin>" || path === "<inline>") return path;
|
|
1778
|
+
return path.replace(/\\/g, "/");
|
|
1779
|
+
}
|
|
1780
|
+
function ruleFromFinding(f) {
|
|
1781
|
+
return {
|
|
1782
|
+
id: f.id,
|
|
1783
|
+
name: f.title,
|
|
1784
|
+
shortDescription: { text: f.title },
|
|
1785
|
+
fullDescription: { text: f.impact },
|
|
1786
|
+
defaultConfiguration: { level: levelForSeverity(f.severity) },
|
|
1787
|
+
properties: {
|
|
1788
|
+
symbol: f.symbol,
|
|
1789
|
+
riskClass: f.riskClass,
|
|
1790
|
+
tags: ["calllint", f.symbol.toLowerCase(), f.riskClass.toLowerCase()]
|
|
1791
|
+
}
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
function resultFromFinding(f, report, configUri) {
|
|
1795
|
+
const ev = f.evidence[0];
|
|
1796
|
+
const region = ev?.line ? { startLine: ev.line } : void 0;
|
|
1797
|
+
const messageParts = [f.impact];
|
|
1798
|
+
if (f.fix) messageParts.push(`Fix: ${f.fix}`);
|
|
1799
|
+
if (f.falsePositiveNote) messageParts.push(`Note: ${f.falsePositiveNote}`);
|
|
1800
|
+
return {
|
|
1801
|
+
ruleId: f.id,
|
|
1802
|
+
level: levelForSeverity(f.severity),
|
|
1803
|
+
message: { text: messageParts.join(" ") },
|
|
1804
|
+
locations: [
|
|
1805
|
+
{
|
|
1806
|
+
physicalLocation: {
|
|
1807
|
+
artifactLocation: { uri: ev?.path ? toUri(ev.path) : configUri },
|
|
1808
|
+
...region ? { region } : {}
|
|
1809
|
+
},
|
|
1810
|
+
logicalLocations: [{ name: report.target.name, kind: "namespace" }]
|
|
1811
|
+
}
|
|
1812
|
+
],
|
|
1813
|
+
partialFingerprints: {
|
|
1814
|
+
configHash: report.fingerprints.configHash,
|
|
1815
|
+
riskSurfaceHash: report.fingerprints.riskSurfaceHash
|
|
1816
|
+
},
|
|
1817
|
+
properties: {
|
|
1818
|
+
server: report.target.name,
|
|
1819
|
+
verdict: report.verdict,
|
|
1820
|
+
symbol: f.symbol,
|
|
1821
|
+
riskClass: f.riskClass,
|
|
1822
|
+
mode: f.mode,
|
|
1823
|
+
confidence: f.confidence,
|
|
1824
|
+
blocker: f.blocker
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
}
|
|
1828
|
+
function renderSarif(summary) {
|
|
1829
|
+
const configUri = toUri(summary.configPath);
|
|
1830
|
+
const rules = /* @__PURE__ */ new Map();
|
|
1831
|
+
const results = [];
|
|
1832
|
+
for (const report of summary.reports) {
|
|
1833
|
+
for (const f of report.findings) {
|
|
1834
|
+
if (!rules.has(f.id)) rules.set(f.id, ruleFromFinding(f));
|
|
1835
|
+
results.push(resultFromFinding(f, report, configUri));
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
const sarif = {
|
|
1839
|
+
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
1840
|
+
version: "2.1.0",
|
|
1841
|
+
runs: [
|
|
1842
|
+
{
|
|
1843
|
+
tool: {
|
|
1844
|
+
driver: {
|
|
1845
|
+
name: "CallLint",
|
|
1846
|
+
informationUri: "https://calllint.com",
|
|
1847
|
+
rules: [...rules.values()]
|
|
1848
|
+
}
|
|
1849
|
+
},
|
|
1850
|
+
results,
|
|
1851
|
+
properties: {
|
|
1852
|
+
aggregateVerdict: summary.verdict,
|
|
1853
|
+
counts: summary.counts,
|
|
1854
|
+
generatedAt: summary.generatedAt
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
]
|
|
1858
|
+
};
|
|
1859
|
+
return JSON.stringify(sarif, null, 2);
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// ../../packages/report-renderer/src/logoBase64.ts
|
|
1863
|
+
var LOGO_REPORT_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAMAAABg3Am1AAADAFBMVEX+/v7+/v3VAADbChn+///9/f39+/v////+/f3+/Pz9/PzYAAraChn+/f7++/zWAADXAAfbDRzXAAraCxn9+vvbDBv9+vrZARHaDh3YAA7aCxvaDRz87O7bDhzZAQ/fKDXZBhTXAA3bEB/XAAX99vfcGCbaAxLaBhXYAAj9+fnnXmfZBQ/aCRb9/f7bChnZBBTaDRrfJTHZAA387u7eIC763+DcFiL519r41tjYAA3wnKLxqK3YAAvWAAbiPUjcEyD9+Pn76evZAxDZBRP3ys3pcXnnY2zkS1b//v776+zcGSbiO0biRE/ulJvqcnvXAALjQk39/v7tipHZCRj++vvaCRj+/P3qdn7pbXXcFCL75+noanPUAADkUlzxpqzoZ3D76Or409bYAAz2xsn53d/++vr3yczyrbH41NfnYmzfKjb52NrlVWD++PjmYGjWAAPyqq/dIy/86uzsgIfZCBbWAALaDhnkTVjiPUnyr7P89PXiQEzYAAPkRlL2xMjlU17hOUX1wcX2x8rtjJLjSVTpb3jcDh3lTFfgMT30trvsgorlUVzqe4Ppa3P0ub3siZDtjpTZAArth43ztLjre4P64eT75ebdJDHaExvvkZjeJDD98vPdHyz++frreIDtjJPvmaD5293qd3/vmqDnY23fMz/rfYbnYGraFiLmX2jdGyfaFh/gLTnkUVvnZW740dPhPUjpbnbumJ7reYL87u/hOUTiSVLlVmHbEB753N/fLzzcER/eLDbiQEr64OL88PH52dvnaHHXAAnbCRn0vcHzsrb+//7zsbbjUFfwpKnjTFLrdn71ur7lW2X++fr89fbqdH398/PztbndHivzrbP0trzuipHwnqT3ztH75ujwo6jgNUHyqq/52tzwoabtjpX2zND//f7vkpjqfYTeMz3vlZv1vMDdJjTdJzPnXWjzsrfdIi3tjJXzsrnqdHzhOEL1vMHkVWDZEB7ulZvVAALnbXXaFiTdGCbtkpnWAQr99fXZDBbshIv1wcblWGPhP0ryJPiiAAAACXBIWXMAAAsSAAALEgHS3X78AAAEpUlEQVRIx41WB1QUVxSdnf0798/u4hbqsouURYrAAQkR6bjUpYurBBQFg6DYIFgCBhRL7L1jj6iADdTYY0mvJ72YnPQeU000MYkxw4ZDwB0W35wz55+ZV+79/857wzA9TcaEdK8YRsLYsRHC1W0hzAgZZbqDRcw2lyXRTnZq84TDR3tgkTH9WLcDT05u2fEQ4Xs/p31X5HBFrZ4Mjt5Teoa64I8ZmXd+2w0nagd2dxDPIfpWwuWzLbHHwcnvzmbjT10csPbWX7cvljXM1q4B40QldjfVSQ5D86xDjdPOXfizoWXWt2bwTg72mIB+56laenl7u6sp89Le/ew3tRR9HoGMEt+KemNQjiXy8MCZwa6Vw+hjQXFZFa8SKutypN3grazkOBvsPa8U+6576drzooLdjmPSae+wYeBtklvXlJK6r88hea7WpPAs21SpMGnby8n+al8iE6fBJZISn0xfT6PGM6WIkJLXKvXGLYHtCZtJIifOOB3L2Y3m82ED18Mil3NYo3MvvPq5qgbp6XdroRMSX5U91dhm+HKds1dCKXieRBZ6eaz77GSb8ZfscYTa6IFhnshNqr8YmRGqdFM8gAEcFj+pcfQfNOX+eq8ZP4G3lTjJcjtGGiNM7k9JQyMMhBiaQh1HOSdVNyK58GHRgLk+6TlqL41SKpX6LQOednUUVhovdUpixuM2ATJrQOCcCR6mkXqlR0A+kB+ll2pG6dyi0uoGzRevcJ47bYxfaQr393BdBYyJOuM/wXTiTu483wzxgDwf7h/Verw8p9BP9SLB82OTbqatxt/sPN/OChImRCahXTslk1hJn+GO/G4mDMqeWTuOkJiFz5XhUzI8I833Zry1gqSHvCmlmONYl1oLjnNhAEGiwu1ZF4sctdMvXT/VG5L13DgcDDd0HAXHAwPMBobcZ5YDcidcuVHi3WrLQeaCA4+UZrNfwbLgpUVbXiGYHLGoYMFE/Mz+OomtAdf7y5R0VhinimlQDb66IlRhiksjKJ5hig1dUdKqul3FxnQGSHoIXJAvjymxq8xuGUMCpGFuusFAqtZHo1Q0tfkYxnuX2kJiKHE5fG3nEkWwcNRh2gJgpVavlDoHK5YEVlfyRKTNcDiRWx4T+6hSKagvGnhB5y6oxENRlazaCLEPgscxReu0piRnqaC5ZGCY80ip1NlvyLTWCe+JIOoEhSPasugAvdLddWmg0L/jB7pLNQHRe72LIbf1ttJuCJjqEK/ThymWg3PCQoVGr8scPVX9oEiA5L+uMV71RmTYTP+hZeBHY3OGv9SxdAM7XhyQFVPRm0OHv69z3YZAygRin1/4u+aWHUXos4tzeFudhwNxg8vBy/FJs3oXyVKvhqV3O6b/y1BmQT6bisXszI+BbEd2DI6O3Q3OzqijctLBpuBHtbZimc7YgQL2RheBviYqpfx0Ngc1xmueubsE/+kWGe1nztGJzQKqPUGKbbjCHtzJ0P4mI09pzthTEzdUORSzKTzlmX6N8vg+7sKmki9mvYPRlLkHE4boD1u3Hpr9VtcY7T9IGKMfzv9gCuwPq17Tl8Pr2+3tvyh1eZ8vHWz+PCRdnUTM/gUe0BB8YRiSMgAAAABJRU5ErkJggg==";
|
|
1864
|
+
|
|
1865
|
+
// ../../packages/report-renderer/src/renderHtml.ts
|
|
1866
|
+
function esc(value) {
|
|
1867
|
+
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1868
|
+
}
|
|
1869
|
+
var VERDICT_COLOR = {
|
|
1870
|
+
SAFE: "#1a7f37",
|
|
1871
|
+
REVIEW: "#9a6700",
|
|
1872
|
+
BLOCK: "#cf222e",
|
|
1873
|
+
UNKNOWN: "#6e7781"
|
|
1874
|
+
};
|
|
1875
|
+
function badge(verdict) {
|
|
1876
|
+
return `<span class="badge" style="background:${VERDICT_COLOR[verdict]}">${esc(verdict)}</span>`;
|
|
1877
|
+
}
|
|
1878
|
+
function findingRow(f) {
|
|
1879
|
+
const ev = f.evidence.map((e) => {
|
|
1880
|
+
const loc = e.path ? ` (${esc(e.path)}${e.line ? `:${e.line}` : ""})` : "";
|
|
1881
|
+
const detail = e.value ?? e.snippet ?? "";
|
|
1882
|
+
return `<li><code>${esc(e.type)}</code> ${esc(e.key ?? "")}${detail ? ` = ${esc(detail)}` : ""}${loc}</li>`;
|
|
1883
|
+
}).join("");
|
|
1884
|
+
return `
|
|
1885
|
+
<tr class="sev-${esc(f.severity)}">
|
|
1886
|
+
<td>${f.blocker ? "\u26D4 " : ""}${esc(f.title)}</td>
|
|
1887
|
+
<td><code>${esc(f.id)}</code></td>
|
|
1888
|
+
<td>${esc(RISK_SYMBOL_LABEL[f.symbol])} <small>(${esc(f.symbol)})</small></td>
|
|
1889
|
+
<td>${esc(f.riskClass)}</td>
|
|
1890
|
+
<td>${esc(f.severity)}</td>
|
|
1891
|
+
<td>${esc(f.mode)}</td>
|
|
1892
|
+
<td>
|
|
1893
|
+
<div class="impact">${esc(f.impact)}</div>
|
|
1894
|
+
<div class="fix"><strong>Fix:</strong> ${esc(f.fix)}</div>
|
|
1895
|
+
${f.evidence.length ? `<ul class="evidence">${ev}</ul>` : ""}
|
|
1896
|
+
${f.falsePositiveNote ? `<div class="fp"><em>${esc(f.falsePositiveNote)}</em></div>` : ""}
|
|
1897
|
+
</td>
|
|
1898
|
+
</tr>`;
|
|
1899
|
+
}
|
|
1900
|
+
function serverCard(r) {
|
|
1901
|
+
const symbols = r.symbols.map((s) => `<span class="sym">${esc(RISK_SYMBOL_LABEL[s])}</span>`).join(" ");
|
|
1902
|
+
const findings = r.findings.length ? `<table class="findings">
|
|
1903
|
+
<thead><tr><th>Finding</th><th>Rule</th><th>Symbol</th><th>Class</th><th>Severity</th><th>Mode</th><th>Detail</th></tr></thead>
|
|
1904
|
+
<tbody>${r.findings.map(findingRow).join("")}</tbody>
|
|
1905
|
+
</table>` : `<p class="none">No findings.</p>`;
|
|
1906
|
+
return `
|
|
1907
|
+
<section class="server">
|
|
1908
|
+
<h2>${badge(r.verdict)} ${esc(r.target.name)}</h2>
|
|
1909
|
+
<p class="meta">
|
|
1910
|
+
Class <strong>${esc(r.riskClass)}</strong> ${esc(RISK_CLASS_LABEL[r.riskClass])}
|
|
1911
|
+
\xB7 confidence ${esc(r.confidence)}
|
|
1912
|
+
\xB7 reproducibility ${esc(r.reproducibility.level)}
|
|
1913
|
+
</p>
|
|
1914
|
+
<p class="symbols">${symbols || '<span class="sym none">no risk symbols</span>'}</p>
|
|
1915
|
+
<p class="summary">${esc(r.summary)}</p>
|
|
1916
|
+
${findings}
|
|
1917
|
+
<p class="policy">autonomous use: <strong>${esc(r.policy.autonomousUse)}</strong>
|
|
1918
|
+
\xB7 manual approval: <strong>${esc(r.policy.manualApproval)}</strong>
|
|
1919
|
+
\xB7 sandbox: <strong>${esc(r.policy.sandbox)}</strong></p>
|
|
1920
|
+
</section>`;
|
|
1921
|
+
}
|
|
1922
|
+
var LOGO_SRC = `data:image/png;base64,${LOGO_REPORT_BASE64}`;
|
|
1923
|
+
var STYLE = `
|
|
1924
|
+
:root { font-family: -apple-system, Segoe UI, Roboto, sans-serif; --brand: #c41e3a; --brand-glow: rgba(196, 30, 58, 0.35); }
|
|
1925
|
+
body { margin: 0; background: #f6f8fa; color: #1f2328; }
|
|
1926
|
+
header { background: #24292f; color: #fff; padding: 24px 32px; }
|
|
1927
|
+
.header-row { display: flex; align-items: center; gap: 14px; }
|
|
1928
|
+
.brand-mark { width: 40px; height: 40px; flex: 0 0 auto; animation: mark-enter 0.6s cubic-bezier(0.22, 1, 0.36, 1) both, mark-glow 4s ease-in-out 0.6s infinite; }
|
|
1929
|
+
header h1 { margin: 0 0 4px; font-size: 20px; }
|
|
1930
|
+
header .sub { color: #b1b8c0; font-size: 13px; }
|
|
1931
|
+
@keyframes mark-enter { from { opacity: 0; transform: scale(0.88); } to { opacity: 1; transform: scale(1); } }
|
|
1932
|
+
@keyframes mark-glow { 0%, 100% { filter: drop-shadow(0 0 0 transparent); } 50% { filter: drop-shadow(0 0 8px var(--brand-glow)); } }
|
|
1933
|
+
@media (prefers-reduced-motion: reduce) { .brand-mark { animation: none; } }
|
|
1934
|
+
.counts { padding: 16px 32px; background: #fff; border-bottom: 1px solid #d0d7de; }
|
|
1935
|
+
.counts span { margin-right: 16px; font-size: 14px; }
|
|
1936
|
+
main { padding: 24px 32px; max-width: 1100px; }
|
|
1937
|
+
.server { background: #fff; border: 1px solid #d0d7de; border-radius: 8px; padding: 20px 24px; margin-bottom: 20px; }
|
|
1938
|
+
.server h2 { margin: 0 0 8px; font-size: 17px; }
|
|
1939
|
+
.badge { color: #fff; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; vertical-align: middle; }
|
|
1940
|
+
.meta, .symbols, .summary, .policy { font-size: 13px; color: #57606a; margin: 6px 0; }
|
|
1941
|
+
.sym { background: #eaeef2; border-radius: 4px; padding: 2px 8px; font-size: 12px; }
|
|
1942
|
+
.sym.none { background: transparent; color: #8c959f; }
|
|
1943
|
+
table.findings { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 13px; }
|
|
1944
|
+
table.findings th, table.findings td { border: 1px solid #d0d7de; padding: 6px 8px; text-align: left; vertical-align: top; }
|
|
1945
|
+
table.findings th { background: #f6f8fa; }
|
|
1946
|
+
tr.sev-critical td:first-child, tr.sev-high td:first-child { border-left: 3px solid #cf222e; }
|
|
1947
|
+
.evidence { margin: 6px 0; padding-left: 18px; }
|
|
1948
|
+
.fix { color: #1a7f37; margin-top: 4px; }
|
|
1949
|
+
.fp { color: #8c959f; margin-top: 4px; }
|
|
1950
|
+
.none { color: #1a7f37; font-style: italic; }
|
|
1951
|
+
code { background: #eff1f3; padding: 1px 4px; border-radius: 3px; font-size: 12px; }
|
|
1952
|
+
footer { padding: 16px 32px; color: #8c959f; font-size: 12px; }
|
|
1953
|
+
`;
|
|
1954
|
+
function renderHtml(summary) {
|
|
1955
|
+
const c = summary.counts;
|
|
1956
|
+
const cards = summary.reports.map(serverCard).join("");
|
|
1957
|
+
return `<!doctype html>
|
|
1958
|
+
<html lang="en">
|
|
1959
|
+
<head>
|
|
1960
|
+
<meta charset="utf-8">
|
|
1961
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1962
|
+
<title>CallLint report \u2014 ${esc(summary.configPath)}</title>
|
|
1963
|
+
<style>${STYLE}</style>
|
|
1964
|
+
</head>
|
|
1965
|
+
<body>
|
|
1966
|
+
<header>
|
|
1967
|
+
<div class="header-row">
|
|
1968
|
+
<img class="brand-mark" src="${LOGO_SRC}" width="40" height="40" alt="CallLint">
|
|
1969
|
+
<div>
|
|
1970
|
+
<h1>CallLint report ${badge(summary.verdict)}</h1>
|
|
1971
|
+
<div class="sub">${esc(summary.configPath)} \xB7 generated ${esc(summary.generatedAt)}</div>
|
|
1972
|
+
</div>
|
|
1973
|
+
</div>
|
|
1974
|
+
</header>
|
|
1975
|
+
<div class="counts">
|
|
1976
|
+
<span>\u26D4 BLOCK: <strong>${c.BLOCK}</strong></span>
|
|
1977
|
+
<span>\u25C7 UNKNOWN: <strong>${c.UNKNOWN}</strong></span>
|
|
1978
|
+
<span>\u26A0 REVIEW: <strong>${c.REVIEW}</strong></span>
|
|
1979
|
+
<span>\u{1F6E1} SAFE: <strong>${c.SAFE}</strong></span>
|
|
1980
|
+
</div>
|
|
1981
|
+
<main>${cards}</main>
|
|
1982
|
+
<footer>CallLint \xB7 evidence-backed verdicts for agent tools \xB7 static analysis, no server executed</footer>
|
|
1983
|
+
</body>
|
|
1984
|
+
</html>`;
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
// src/exitCode.ts
|
|
1988
|
+
function exitCodeFor(summary, policy) {
|
|
1989
|
+
const verdict = summary.verdict;
|
|
1990
|
+
if (!shouldFailCi(verdict, policy)) return EXIT.OK;
|
|
1991
|
+
switch (verdict) {
|
|
1992
|
+
case "BLOCK":
|
|
1993
|
+
return EXIT.BLOCK;
|
|
1994
|
+
case "UNKNOWN":
|
|
1995
|
+
return EXIT.UNKNOWN;
|
|
1996
|
+
case "REVIEW":
|
|
1997
|
+
return EXIT.REVIEW;
|
|
1998
|
+
case "SAFE":
|
|
1999
|
+
return EXIT.OK;
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
|
|
2003
|
+
// src/commands/resolveInput.ts
|
|
2004
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
2005
|
+
import { join as join2 } from "node:path";
|
|
2006
|
+
var DEFAULT_CONFIG_PATHS = [
|
|
2007
|
+
".cursor/mcp.json",
|
|
2008
|
+
".mcp.json",
|
|
2009
|
+
"mcp.json",
|
|
2010
|
+
".claude/settings.json",
|
|
2011
|
+
".vscode/mcp.json"
|
|
2012
|
+
];
|
|
2013
|
+
function findDefaultConfig(cwd) {
|
|
2014
|
+
for (const rel of DEFAULT_CONFIG_PATHS) {
|
|
2015
|
+
const p = join2(cwd, rel);
|
|
2016
|
+
if (existsSync2(p)) return p;
|
|
2017
|
+
}
|
|
2018
|
+
return void 0;
|
|
2019
|
+
}
|
|
2020
|
+
function isInputError(v) {
|
|
2021
|
+
return "error" in v;
|
|
2022
|
+
}
|
|
2023
|
+
function resolveConfigInput(args, deps) {
|
|
2024
|
+
if (flagBool(args.flags, "stdin")) {
|
|
2025
|
+
return { text: deps.readStdin(), configPath: "<stdin>" };
|
|
2026
|
+
}
|
|
2027
|
+
const given = args.positionals[0];
|
|
2028
|
+
if (given) {
|
|
2029
|
+
const spec = parseTargetSpec(given);
|
|
2030
|
+
if (spec.kind === "npm") {
|
|
2031
|
+
if (!spec.packageSpec) {
|
|
2032
|
+
return { error: "Empty npm target. Use npm:<package>[@version].", exitCode: EXIT.USAGE };
|
|
2033
|
+
}
|
|
2034
|
+
return synthesizeNpmConfig(spec.packageSpec);
|
|
2035
|
+
}
|
|
2036
|
+
if (spec.kind === "github") {
|
|
2037
|
+
return {
|
|
2038
|
+
error: "GitHub targets require network access. Re-run with --online to fetch repo MCP configs.",
|
|
2039
|
+
exitCode: EXIT.USAGE
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
}
|
|
2043
|
+
const resolved = given ?? findDefaultConfig(deps.cwd);
|
|
2044
|
+
if (!resolved) {
|
|
2045
|
+
return {
|
|
2046
|
+
error: "No config given and none found. Pass a path or use --stdin.\nLooked in: " + DEFAULT_CONFIG_PATHS.join(", "),
|
|
2047
|
+
exitCode: EXIT.USAGE
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
if (!existsSync2(resolved)) {
|
|
2051
|
+
return { error: `File not found: ${resolved}`, exitCode: EXIT.USAGE };
|
|
2052
|
+
}
|
|
2053
|
+
return { text: readFileSync3(resolved, "utf8"), configPath: resolved };
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/commands/scan.ts
|
|
2057
|
+
function scanCommand(args, deps) {
|
|
2058
|
+
const policyPath = flagStr(args.flags, "policy");
|
|
2059
|
+
let policy;
|
|
2060
|
+
try {
|
|
2061
|
+
policy = loadPolicyOrDefault(policyPath);
|
|
2062
|
+
} catch (err) {
|
|
2063
|
+
return {
|
|
2064
|
+
stdout: "",
|
|
2065
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
2066
|
+
exitCode: EXIT.ERROR
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
const input = deps.online?.inputOverride ?? resolveConfigInput(args, deps);
|
|
2070
|
+
if (isInputError(input)) {
|
|
2071
|
+
return { stdout: "", stderr: input.error, exitCode: input.exitCode };
|
|
2072
|
+
}
|
|
2073
|
+
const { text, configPath } = input;
|
|
2074
|
+
let summary;
|
|
2075
|
+
try {
|
|
2076
|
+
summary = scanConfigText(text, configPath, {
|
|
2077
|
+
policy,
|
|
2078
|
+
now: deps.now,
|
|
2079
|
+
generatedAt: deps.generatedAt,
|
|
2080
|
+
extraFindings: deps.online?.extraFindings
|
|
2081
|
+
});
|
|
2082
|
+
} catch (err) {
|
|
2083
|
+
if (err instanceof ConfigParseError) {
|
|
2084
|
+
return {
|
|
2085
|
+
stdout: "",
|
|
2086
|
+
stderr: `Parse error in ${configPath}: ${err.message}`,
|
|
2087
|
+
exitCode: EXIT.ERROR
|
|
2088
|
+
};
|
|
2089
|
+
}
|
|
2090
|
+
throw err;
|
|
2091
|
+
}
|
|
2092
|
+
if (deps.writeCacheFile !== false) {
|
|
2093
|
+
try {
|
|
2094
|
+
writeCache(summary, join3(deps.cwd, ".calllint", "last-scan.json"));
|
|
2095
|
+
} catch {
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
|
|
2099
|
+
let stdout;
|
|
2100
|
+
if (flagBool(args.flags, "json")) stdout = renderJson(summary);
|
|
2101
|
+
else if (flagBool(args.flags, "sarif")) stdout = renderSarif(summary);
|
|
2102
|
+
else if (flagBool(args.flags, "html")) stdout = renderHtml(summary);
|
|
2103
|
+
else if (flagBool(args.flags, "compact")) stdout = renderCompact(summary, style);
|
|
2104
|
+
else stdout = renderTerminal(summary, style);
|
|
2105
|
+
const exitCode = flagBool(args.flags, "ci") ? exitCodeFor(summary, policy) : EXIT.OK;
|
|
2106
|
+
return { stdout, exitCode };
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// src/commands/explain.ts
|
|
2110
|
+
import { join as join4 } from "node:path";
|
|
2111
|
+
function explainCommand(args, deps) {
|
|
2112
|
+
const serverName = args.positionals[0];
|
|
2113
|
+
if (!serverName) {
|
|
2114
|
+
return {
|
|
2115
|
+
stdout: "",
|
|
2116
|
+
stderr: "Usage: calllint explain <server>",
|
|
2117
|
+
exitCode: EXIT.USAGE
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
const summary = readCache(join4(deps.cwd, ".calllint", "last-scan.json"));
|
|
2121
|
+
if (!summary) {
|
|
2122
|
+
return {
|
|
2123
|
+
stdout: "",
|
|
2124
|
+
stderr: "No cached scan found. Run `calllint scan` first.",
|
|
2125
|
+
exitCode: EXIT.ERROR
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
const report = summary.reports.find((r) => r.target.name === serverName);
|
|
2129
|
+
if (!report) {
|
|
2130
|
+
const names = summary.reports.map((r) => r.target.name).join(", ");
|
|
2131
|
+
return {
|
|
2132
|
+
stdout: "",
|
|
2133
|
+
stderr: `Server "${serverName}" not in last scan. Available: ${names || "(none)"}`,
|
|
2134
|
+
exitCode: EXIT.USAGE
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
|
|
2138
|
+
return { stdout: renderExplain(report, style), exitCode: EXIT.OK };
|
|
2139
|
+
}
|
|
2140
|
+
|
|
2141
|
+
// src/commands/policy.ts
|
|
2142
|
+
import { existsSync as existsSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2143
|
+
import { join as join5 } from "node:path";
|
|
2144
|
+
function policyCommand(args, deps) {
|
|
2145
|
+
const sub = args.positionals[0];
|
|
2146
|
+
if (sub === "init") {
|
|
2147
|
+
const path = join5(deps.cwd, "calllint.policy.json");
|
|
2148
|
+
if (existsSync3(path) && !flagBool(args.flags, "force")) {
|
|
2149
|
+
return {
|
|
2150
|
+
stdout: "",
|
|
2151
|
+
stderr: `${path} already exists. Use --force to overwrite.`,
|
|
2152
|
+
exitCode: EXIT.USAGE
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
writeFileSync2(path, defaultPolicyJson(), "utf8");
|
|
2156
|
+
return { stdout: `Wrote default policy to ${path}`, exitCode: EXIT.OK };
|
|
2157
|
+
}
|
|
2158
|
+
if (sub === "explain") {
|
|
2159
|
+
try {
|
|
2160
|
+
const policy = loadPolicyOrDefault(flagStr(args.flags, "policy"));
|
|
2161
|
+
return { stdout: JSON.stringify(policy, null, 2), exitCode: EXIT.OK };
|
|
2162
|
+
} catch (err) {
|
|
2163
|
+
return {
|
|
2164
|
+
stdout: "",
|
|
2165
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
2166
|
+
exitCode: EXIT.ERROR
|
|
2167
|
+
};
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
return {
|
|
2171
|
+
stdout: "",
|
|
2172
|
+
stderr: "Usage: calllint policy <init|explain>",
|
|
2173
|
+
exitCode: EXIT.USAGE
|
|
2174
|
+
};
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2177
|
+
// src/commands/verify.ts
|
|
2178
|
+
import { join as join6 } from "node:path";
|
|
2179
|
+
function loadPolicy(args) {
|
|
2180
|
+
try {
|
|
2181
|
+
return loadPolicyOrDefault(flagStr(args.flags, "policy"));
|
|
2182
|
+
} catch (err) {
|
|
2183
|
+
return { error: err instanceof Error ? err.message : String(err) };
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
function baselinePathFor(args, cwd) {
|
|
2187
|
+
return flagStr(args.flags, "baseline") ?? join6(cwd, ".calllint", "baseline.json");
|
|
2188
|
+
}
|
|
2189
|
+
function baselineCommand(args, deps) {
|
|
2190
|
+
const policy = loadPolicy(args);
|
|
2191
|
+
if ("error" in policy) return { stdout: "", stderr: policy.error, exitCode: EXIT.ERROR };
|
|
2192
|
+
const input = deps.online?.inputOverride ?? resolveConfigInput(args, deps);
|
|
2193
|
+
if (isInputError(input)) return { stdout: "", stderr: input.error, exitCode: input.exitCode };
|
|
2194
|
+
let summary;
|
|
2195
|
+
try {
|
|
2196
|
+
summary = scanConfigText(input.text, input.configPath, {
|
|
2197
|
+
policy,
|
|
2198
|
+
generatedAt: deps.generatedAt,
|
|
2199
|
+
extraFindings: deps.online?.extraFindings
|
|
2200
|
+
});
|
|
2201
|
+
} catch (err) {
|
|
2202
|
+
if (err instanceof ConfigParseError) {
|
|
2203
|
+
return { stdout: "", stderr: `Parse error in ${input.configPath}: ${err.message}`, exitCode: EXIT.ERROR };
|
|
2204
|
+
}
|
|
2205
|
+
throw err;
|
|
2206
|
+
}
|
|
2207
|
+
const baseline = buildBaseline(summary, deps.generatedAt);
|
|
2208
|
+
const path = baselinePathFor(args, deps.cwd);
|
|
2209
|
+
if (deps.writeBaselineFile !== false) {
|
|
2210
|
+
try {
|
|
2211
|
+
writeBaseline(baseline, path);
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
return { stdout: "", stderr: `Could not write baseline: ${String(err)}`, exitCode: EXIT.ERROR };
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
if (flagBool(args.flags, "json")) {
|
|
2217
|
+
return { stdout: JSON.stringify(baseline, null, 2), exitCode: EXIT.OK };
|
|
2218
|
+
}
|
|
2219
|
+
const n = baseline.entries.length;
|
|
2220
|
+
return {
|
|
2221
|
+
stdout: `Baseline written for ${n} server${n === 1 ? "" : "s"} \u2192 ${path}`,
|
|
2222
|
+
exitCode: EXIT.OK
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
function verifyCommand(args, deps) {
|
|
2226
|
+
const policy = loadPolicy(args);
|
|
2227
|
+
if ("error" in policy) return { stdout: "", stderr: policy.error, exitCode: EXIT.ERROR };
|
|
2228
|
+
const path = baselinePathFor(args, deps.cwd);
|
|
2229
|
+
const baseline = readBaseline(path);
|
|
2230
|
+
if (!baseline) {
|
|
2231
|
+
return {
|
|
2232
|
+
stdout: "",
|
|
2233
|
+
stderr: `No baseline found at ${path}. Run \`calllint baseline\` first.`,
|
|
2234
|
+
exitCode: EXIT.ERROR
|
|
2235
|
+
};
|
|
2236
|
+
}
|
|
2237
|
+
const input = deps.online?.inputOverride ?? resolveConfigInput(args, deps);
|
|
2238
|
+
if (isInputError(input)) return { stdout: "", stderr: input.error, exitCode: input.exitCode };
|
|
2239
|
+
let summary;
|
|
2240
|
+
try {
|
|
2241
|
+
summary = scanConfigText(input.text, input.configPath, {
|
|
2242
|
+
policy,
|
|
2243
|
+
generatedAt: deps.generatedAt,
|
|
2244
|
+
extraFindings: deps.online?.extraFindings
|
|
2245
|
+
});
|
|
2246
|
+
} catch (err) {
|
|
2247
|
+
if (err instanceof ConfigParseError) {
|
|
2248
|
+
return { stdout: "", stderr: `Parse error in ${input.configPath}: ${err.message}`, exitCode: EXIT.ERROR };
|
|
2249
|
+
}
|
|
2250
|
+
throw err;
|
|
2251
|
+
}
|
|
2252
|
+
const drift = computeDrift(baseline, summary, deps.generatedAt);
|
|
2253
|
+
const stdout = flagBool(args.flags, "json") ? renderDriftJson(drift) : renderDrift(drift);
|
|
2254
|
+
const exitCode = flagBool(args.flags, "ci") && drift.drifted ? EXIT.DRIFT : EXIT.OK;
|
|
2255
|
+
return { stdout, exitCode };
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
// src/run.ts
|
|
2259
|
+
function run(argv, deps) {
|
|
2260
|
+
const args = parseArgs(argv);
|
|
2261
|
+
const cmd = args.command;
|
|
2262
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
2263
|
+
return helpCommand();
|
|
2264
|
+
}
|
|
2265
|
+
switch (cmd) {
|
|
2266
|
+
case "scan":
|
|
2267
|
+
return scanCommand(args, {
|
|
2268
|
+
cwd: deps.cwd,
|
|
2269
|
+
readStdin: deps.readStdin,
|
|
2270
|
+
now: deps.now,
|
|
2271
|
+
generatedAt: deps.generatedAt,
|
|
2272
|
+
writeCacheFile: deps.writeCacheFile,
|
|
2273
|
+
online: deps.online
|
|
2274
|
+
});
|
|
2275
|
+
case "baseline":
|
|
2276
|
+
return baselineCommand(args, {
|
|
2277
|
+
cwd: deps.cwd,
|
|
2278
|
+
readStdin: deps.readStdin,
|
|
2279
|
+
generatedAt: deps.generatedAt,
|
|
2280
|
+
writeBaselineFile: deps.writeCacheFile,
|
|
2281
|
+
online: deps.online
|
|
2282
|
+
});
|
|
2283
|
+
case "verify":
|
|
2284
|
+
return verifyCommand(args, {
|
|
2285
|
+
cwd: deps.cwd,
|
|
2286
|
+
readStdin: deps.readStdin,
|
|
2287
|
+
generatedAt: deps.generatedAt,
|
|
2288
|
+
writeBaselineFile: deps.writeCacheFile,
|
|
2289
|
+
online: deps.online
|
|
2290
|
+
});
|
|
2291
|
+
case "explain":
|
|
2292
|
+
return explainCommand(args, { cwd: deps.cwd });
|
|
2293
|
+
case "policy":
|
|
2294
|
+
return policyCommand(args, { cwd: deps.cwd });
|
|
2295
|
+
default:
|
|
2296
|
+
return {
|
|
2297
|
+
stdout: "",
|
|
2298
|
+
stderr: `Unknown command: ${cmd}
|
|
2299
|
+
Run \`calllint help\`.`,
|
|
2300
|
+
exitCode: 2
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// ../../packages/online/src/npm.ts
|
|
2306
|
+
function isRecord3(v) {
|
|
2307
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
2308
|
+
}
|
|
2309
|
+
function parseSpec(packageSpec) {
|
|
2310
|
+
if (packageSpec.startsWith("@")) {
|
|
2311
|
+
const slash = packageSpec.indexOf("/");
|
|
2312
|
+
const at2 = packageSpec.indexOf("@", slash);
|
|
2313
|
+
if (at2 === -1) return { name: packageSpec };
|
|
2314
|
+
return { name: packageSpec.slice(0, at2), version: packageSpec.slice(at2 + 1) || void 0 };
|
|
2315
|
+
}
|
|
2316
|
+
const at = packageSpec.indexOf("@");
|
|
2317
|
+
if (at <= 0) return { name: packageSpec };
|
|
2318
|
+
return { name: packageSpec.slice(0, at), version: packageSpec.slice(at + 1) || void 0 };
|
|
2319
|
+
}
|
|
2320
|
+
var INSTALL_SCRIPT_KEYS = ["preinstall", "install", "postinstall"];
|
|
2321
|
+
async function fetchNpmFacts(packageSpec, fetchJson) {
|
|
2322
|
+
const { name, version } = parseSpec(packageSpec);
|
|
2323
|
+
const url = `https://registry.npmjs.org/${name.replace(/\//g, "%2f")}`;
|
|
2324
|
+
let doc;
|
|
2325
|
+
try {
|
|
2326
|
+
doc = await fetchJson(url);
|
|
2327
|
+
} catch {
|
|
2328
|
+
return { name, versionExists: false, installScripts: [] };
|
|
2329
|
+
}
|
|
2330
|
+
if (!isRecord3(doc)) return { name, versionExists: false, installScripts: [] };
|
|
2331
|
+
const distTags = isRecord3(doc["dist-tags"]) ? doc["dist-tags"] : {};
|
|
2332
|
+
const latestVersion = typeof distTags.latest === "string" ? distTags.latest : void 0;
|
|
2333
|
+
const versions = isRecord3(doc.versions) ? doc.versions : {};
|
|
2334
|
+
const isFloating = !version || version === "latest" || /[\^~><*]/.test(version);
|
|
2335
|
+
const resolvedVersion = isFloating ? latestVersion : version;
|
|
2336
|
+
const versionDoc = resolvedVersion && isRecord3(versions[resolvedVersion]) ? versions[resolvedVersion] : void 0;
|
|
2337
|
+
if (!versionDoc) {
|
|
2338
|
+
return { name, versionExists: false, installScripts: [], latestVersion, resolvedVersion };
|
|
2339
|
+
}
|
|
2340
|
+
const scripts = isRecord3(versionDoc.scripts) ? versionDoc.scripts : {};
|
|
2341
|
+
const installScripts = INSTALL_SCRIPT_KEYS.filter(
|
|
2342
|
+
(k) => typeof scripts[k] === "string" && scripts[k].length > 0
|
|
2343
|
+
);
|
|
2344
|
+
const deprecated = typeof versionDoc.deprecated === "string" ? versionDoc.deprecated : void 0;
|
|
2345
|
+
return {
|
|
2346
|
+
name,
|
|
2347
|
+
versionExists: true,
|
|
2348
|
+
installScripts,
|
|
2349
|
+
deprecated,
|
|
2350
|
+
latestVersion,
|
|
2351
|
+
resolvedVersion
|
|
2352
|
+
};
|
|
2353
|
+
}
|
|
2354
|
+
function findingsFromNpmFacts(facts, fetchedAt) {
|
|
2355
|
+
const findings = [];
|
|
2356
|
+
const stamp = (f) => ({ ...f, source: "online", fetchedAt });
|
|
2357
|
+
if (!facts.versionExists) {
|
|
2358
|
+
findings.push(stamp({
|
|
2359
|
+
id: "supply.version-not-found",
|
|
2360
|
+
title: "Package version not found in registry",
|
|
2361
|
+
severity: "high",
|
|
2362
|
+
blocker: false,
|
|
2363
|
+
symbol: "SUPPLY",
|
|
2364
|
+
riskClass: "S1",
|
|
2365
|
+
mode: "OBSERVED",
|
|
2366
|
+
confidence: "high",
|
|
2367
|
+
detectionMethod: "package-metadata",
|
|
2368
|
+
evidence: [{ type: "package-metadata", key: "registry", value: facts.name }],
|
|
2369
|
+
impact: "The requested package version does not exist on the npm registry, so the configured server cannot be verified or may resolve to something unexpected.",
|
|
2370
|
+
fix: "Pin to a published, existing version of the package."
|
|
2371
|
+
}));
|
|
2372
|
+
return findings;
|
|
2373
|
+
}
|
|
2374
|
+
if (facts.installScripts.length > 0) {
|
|
2375
|
+
findings.push(stamp({
|
|
2376
|
+
id: "supply.install-scripts",
|
|
2377
|
+
title: "Package runs install scripts",
|
|
2378
|
+
severity: "high",
|
|
2379
|
+
blocker: false,
|
|
2380
|
+
symbol: "EXEC",
|
|
2381
|
+
riskClass: "S4",
|
|
2382
|
+
mode: "OBSERVED",
|
|
2383
|
+
confidence: "high",
|
|
2384
|
+
detectionMethod: "package-metadata",
|
|
2385
|
+
evidence: facts.installScripts.map((s) => ({
|
|
2386
|
+
type: "package-metadata",
|
|
2387
|
+
key: "script",
|
|
2388
|
+
value: s
|
|
2389
|
+
})),
|
|
2390
|
+
impact: "npm install/postinstall scripts execute arbitrary code on the host at install time, before the agent ever invokes a tool.",
|
|
2391
|
+
fix: "Review the install scripts, or install with --ignore-scripts and vendor the package.",
|
|
2392
|
+
falsePositiveNote: "Many legitimate packages use postinstall for native builds; review what the script does."
|
|
2393
|
+
}));
|
|
2394
|
+
}
|
|
2395
|
+
if (facts.deprecated) {
|
|
2396
|
+
findings.push(stamp({
|
|
2397
|
+
id: "supply.deprecated",
|
|
2398
|
+
title: "Package version is deprecated",
|
|
2399
|
+
severity: "medium",
|
|
2400
|
+
blocker: false,
|
|
2401
|
+
symbol: "SUPPLY",
|
|
2402
|
+
riskClass: "S1",
|
|
2403
|
+
mode: "OBSERVED",
|
|
2404
|
+
confidence: "high",
|
|
2405
|
+
detectionMethod: "package-metadata",
|
|
2406
|
+
evidence: [{ type: "package-metadata", key: "deprecated", value: facts.deprecated }],
|
|
2407
|
+
impact: "A deprecated package may be unmaintained and miss security fixes.",
|
|
2408
|
+
fix: "Migrate to the maintained successor or a supported version."
|
|
2409
|
+
}));
|
|
2410
|
+
}
|
|
2411
|
+
return findings;
|
|
2412
|
+
}
|
|
2413
|
+
async function enrichNpmPackage(packageSpec, fetchJson, fetchedAt) {
|
|
2414
|
+
const facts = await fetchNpmFacts(packageSpec, fetchJson);
|
|
2415
|
+
return { facts, findings: findingsFromNpmFacts(facts, fetchedAt) };
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// ../../packages/online/src/github.ts
|
|
2419
|
+
var GITHUB_CONFIG_CANDIDATES = [
|
|
2420
|
+
".cursor/mcp.json",
|
|
2421
|
+
".mcp.json",
|
|
2422
|
+
"mcp.json",
|
|
2423
|
+
".vscode/mcp.json",
|
|
2424
|
+
".claude/settings.json"
|
|
2425
|
+
];
|
|
2426
|
+
function rawUrl(repo, ref, path) {
|
|
2427
|
+
return `https://raw.githubusercontent.com/${repo}/${ref}/${path}`;
|
|
2428
|
+
}
|
|
2429
|
+
async function fetchGithubConfig(repo, fetchText, ref = "HEAD") {
|
|
2430
|
+
for (const path of GITHUB_CONFIG_CANDIDATES) {
|
|
2431
|
+
const text = await fetchText(rawUrl(repo, ref, path));
|
|
2432
|
+
if (text && text.trim().length > 0) {
|
|
2433
|
+
return { text, foundPath: path, ref };
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
return { ref };
|
|
2437
|
+
}
|
|
2438
|
+
|
|
2439
|
+
// src/online.ts
|
|
2440
|
+
var realFetchJson = async (url) => {
|
|
2441
|
+
const res = await fetch(url);
|
|
2442
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`);
|
|
2443
|
+
return res.json();
|
|
2444
|
+
};
|
|
2445
|
+
var realFetchText = async (url) => {
|
|
2446
|
+
const res = await fetch(url);
|
|
2447
|
+
if (!res.ok) return void 0;
|
|
2448
|
+
return res.text();
|
|
2449
|
+
};
|
|
2450
|
+
async function computeOnlineEnrichment(argv, deps = {}) {
|
|
2451
|
+
const args = parseArgs(argv);
|
|
2452
|
+
if (!flagBool(args.flags, "online")) return void 0;
|
|
2453
|
+
const given = args.positionals[0];
|
|
2454
|
+
if (!given) return void 0;
|
|
2455
|
+
const spec = parseTargetSpec(given);
|
|
2456
|
+
const fetchJson = deps.fetchJson ?? realFetchJson;
|
|
2457
|
+
const fetchText = deps.fetchText ?? realFetchText;
|
|
2458
|
+
const fetchedAt = deps.fetchedAt ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2459
|
+
if (spec.kind === "npm" && spec.packageSpec) {
|
|
2460
|
+
const serverName = serverNameForPackage(spec.packageSpec);
|
|
2461
|
+
const { findings } = await enrichNpmPackage(spec.packageSpec, fetchJson, fetchedAt);
|
|
2462
|
+
return { extraFindings: { [serverName]: findings } };
|
|
2463
|
+
}
|
|
2464
|
+
if (spec.kind === "github" && spec.repo) {
|
|
2465
|
+
const result = await fetchGithubConfig(spec.repo, fetchText, spec.ref ?? "HEAD");
|
|
2466
|
+
if (result.text) {
|
|
2467
|
+
return {
|
|
2468
|
+
inputOverride: {
|
|
2469
|
+
text: result.text,
|
|
2470
|
+
configPath: `github:${spec.repo}@${result.ref}/${result.foundPath}`
|
|
2471
|
+
},
|
|
2472
|
+
note: `fetched ${result.foundPath} @ ${result.ref}`
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
return void 0;
|
|
2476
|
+
}
|
|
2477
|
+
return void 0;
|
|
2478
|
+
}
|
|
2479
|
+
|
|
2480
|
+
// src/clock.ts
|
|
2481
|
+
function resolveClock(argv, fallback) {
|
|
2482
|
+
const { flags } = parseArgs(argv);
|
|
2483
|
+
const raw = flagStr(flags, "generated-at");
|
|
2484
|
+
if (raw !== void 0) {
|
|
2485
|
+
const ms = Date.parse(raw);
|
|
2486
|
+
if (Number.isNaN(ms)) {
|
|
2487
|
+
throw new Error(
|
|
2488
|
+
`Invalid --generated-at value: "${raw}" (expected an ISO 8601 timestamp)`
|
|
2489
|
+
);
|
|
2490
|
+
}
|
|
2491
|
+
return { generatedAt: new Date(ms).toISOString(), now: ms };
|
|
2492
|
+
}
|
|
2493
|
+
const d = fallback();
|
|
2494
|
+
return { generatedAt: d.toISOString(), now: d.getTime() };
|
|
2495
|
+
}
|
|
2496
|
+
|
|
2497
|
+
// src/index.ts
|
|
2498
|
+
function readStdin() {
|
|
2499
|
+
try {
|
|
2500
|
+
return readFileSync4(0, "utf8");
|
|
2501
|
+
} catch {
|
|
2502
|
+
return "";
|
|
2503
|
+
}
|
|
2504
|
+
}
|
|
2505
|
+
async function main() {
|
|
2506
|
+
const argv = process.argv.slice(2);
|
|
2507
|
+
let clock;
|
|
2508
|
+
try {
|
|
2509
|
+
clock = resolveClock(argv, () => /* @__PURE__ */ new Date());
|
|
2510
|
+
} catch (err) {
|
|
2511
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
2512
|
+
`);
|
|
2513
|
+
process.exitCode = 2;
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
const { generatedAt, now } = clock;
|
|
2517
|
+
let online;
|
|
2518
|
+
try {
|
|
2519
|
+
online = await computeOnlineEnrichment(argv, { fetchedAt: generatedAt });
|
|
2520
|
+
} catch (err) {
|
|
2521
|
+
process.stderr.write(`--online failed: ${err instanceof Error ? err.message : String(err)}
|
|
2522
|
+
`);
|
|
2523
|
+
process.exitCode = 3;
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
if (online?.note) process.stderr.write(`online: ${online.note}
|
|
2527
|
+
`);
|
|
2528
|
+
const result = run(argv, {
|
|
2529
|
+
cwd: process.cwd(),
|
|
2530
|
+
readStdin,
|
|
2531
|
+
now,
|
|
2532
|
+
generatedAt,
|
|
2533
|
+
online
|
|
2534
|
+
});
|
|
2535
|
+
if (result.stdout) process.stdout.write(result.stdout + "\n");
|
|
2536
|
+
if (result.stderr) process.stderr.write(result.stderr + "\n");
|
|
2537
|
+
process.exitCode = result.exitCode;
|
|
2538
|
+
}
|
|
2539
|
+
void main();
|